<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="zh_CN">
  <title>HyBlog</title>
  <subtitle>A Blog belonging to Hygen</subtitle>
  <link href="https://www.hygen.red/" rel="alternate" type="text/html"/>
  <link href="https://www.hygen.red/atom.xml" rel="self" type="application/atom+xml"/>
  <id>https://www.hygen.red/</id>
  <updated>2026-07-18T00:00:00.000Z</updated>
  <entry>
    <title>矩阵置零</title>
    <link href="https://www.hygen.red/posts/hot100/06-%E7%9F%A9%E9%98%B5/018%E7%9F%A9%E9%98%B5%E7%BD%AE%E9%9B%B6/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/hot100/06-%E7%9F%A9%E9%98%B5/018%E7%9F%A9%E9%98%B5%E7%BD%AE%E9%9B%B6/</id>
    <published>2026-07-18T00:00:00.000Z</published>
    <updated>2026-07-18T00:00:00.000Z</updated>
    <summary>Leetcode-73</summary>
    <content type="html"><![CDATA[<h1>题目描述</h1>
<p><a href="https://leetcode.cn/problems/set-matrix-zeroes/description/?envType=study-plan-v2&amp;envId=top-100-liked">题目链接</a></p>
<p>给定一个 <code>m x n</code> 的矩阵，如果一个元素为 <code>0</code>，则将其所在行和列的所有元素都设为 <code>0</code>。请使用<strong>原地</strong>算法。</p>
<h2>输入</h2>
<p><code>m x n</code> 的整数矩阵 <code>matrix</code></p>
<h2>输出</h2>
<p>原地修改矩阵，将包含 <code>0</code> 的行和列全部置零</p>
<h2>约束条件</h2>
<ul>
<li><code>m == matrix.length</code></li>
<li><code>n == matrix[0].length</code></li>
<li><code>1 &lt;= m, n &lt;= 200</code></li>
<li><code>-2^31 &lt;= matrix[i][j] &lt;= 2^31 - 1</code></li>
</ul>
<h2>进阶约束</h2>
<p>一个直观的解决方案是使用 $O(mn)$ 的额外空间，但这并不是一个好的解决方案。一个简单的改进方案是使用 $O(m + n)$ 的额外空间，但这仍然不是最好的解决方案。你能想出一个仅使用<strong>常量空间</strong>的解决方案吗？</p>
<h2>示例1</h2>
<pre><code>输入：matrix = [[1,1,1],[1,0,1],[1,1,1]]
输出：[[1,0,1],[0,0,0],[1,0,1]]
</code></pre>
<h2>示例2</h2>
<pre><code>输入：matrix = [[0,1,2,0],[3,4,5,2],[1,3,1,5]]
输出：[[0,0,0,0],[0,4,5,0],[0,3,1,0]]
</code></pre>
<h1>解题思路</h1>
<p>这道题的难点在于一个"连锁反应"陷阱：你不能在遍历过程中直接把行和列置零，因为那些被置零的位置会在后续遍历中被误判为"原本就是 0"，从而错误地波及更多行和列——最终整个矩阵可能全变成 0。</p>
<blockquote>
<p>💡 <strong>核心直觉</strong>：我们需要一种方式来"记住"哪些行和列需要被置零，而不是在遍历过程中立即执行。就像在纸质表格上做标记——先用铅笔圈出哪些行和列有 0，全部圈完后再统一擦除。</p>
</blockquote>
<h2>从暴力解法出发</h2>
<p>最朴素的想法：深拷贝一份矩阵，遍历拷贝来找 0，在原矩阵上执行置零：</p>
<pre><code>class Solution:
    def setZeroes(self, matrix: List[List[int]]) -&gt; None:
        m, n = len(matrix), len(matrix[0])
        copy = [row[:] for row in matrix]

        for i in range(m):
            for j in range(n):
                if copy[i][j] == 0:
                    for k in range(n):
                        matrix[i][k] = 0  # 整行置零
                    for k in range(m):
                        matrix[k][j] = 0  # 整列置零
</code></pre>
<ul>
<li>时间复杂度：$O(mn \cdot (m+n))$——每个 0 都要遍历整行 + 整列</li>
<li>空间复杂度：$O(mn)$——完整副本</li>
</ul>
<p>显然不可接受。</p>
<h2>用两个数组记录零的位置（$O(m+n)$ 空间）</h2>
<p>不复制矩阵，用两个布尔数组分别记录哪些行、哪些列包含 0：</p>
<pre><code>class Solution:
    def setZeroes(self, matrix: List[List[int]]) -&gt; None:
        m, n = len(matrix), len(matrix[0])
        zero_rows = [False] * m
        zero_cols = [False] * n

        # 第一遍：记录
        for i in range(m):
            for j in range(n):
                if matrix[i][j] == 0:
                    zero_rows[i] = True
                    zero_cols[j] = True

        # 第二遍：执行
        for i in range(m):
            for j in range(n):
                if zero_rows[i] or zero_cols[j]:
                    matrix[i][j] = 0
</code></pre>
<ul>
<li>时间复杂度：$O(mn)$</li>
<li>空间复杂度：$O(m + n)$</li>
</ul>
<p>已经很好了，但题目进阶要求是 <strong>$O(1)$ 空间</strong>。数组本身有 $m \times n$ 个位置——能不能借用矩阵自己的一部分来充当标记数组？</p>
<h2>原地标记：用第一行和第一列做"标记牌"（$O(1)$ 空间）</h2>
<p>关键的洞察：只要 $m+n$ 个布尔标记位就够了。矩阵的第一行（$n$ 个元素）加上第一列（$m$ 个元素）刚好够用！</p>
<blockquote>
<p>🧠 <strong>直觉理解</strong>：把第一行当作"列标记牌"——<code>matrix[0][j] = 0</code> 表示"第 j 列需要清零"。把第一列当作"行标记牌"——<code>matrix[i][0] = 0</code> 表示"第 i 行需要清零"。剩下的 <code>[1,m) × [1,n)</code> 区域是真正的"工作区"，我们遍历它、发现 0、在标记牌上记录。</p>
</blockquote>
<p>但有个麻烦事：第一行和第一列既要是"标记牌"，自己本身也可能是数据——如果第一行原本就有 0，它在标记牌上的 <code>0</code> 到底代表"第一行需要清零"还是"某列需要清零"？</p>
<p>所以我们需要<strong>两个独立变量</strong> <code>r</code> 和 <code>c</code>，提前记录第一行和第一列是否原本就含 0。</p>
<h3>算法四步走</h3>
<p><strong>第一步：检查第一行和第一列</strong></p>
<pre><code>m, n = len(matrix), len(matrix[0])
r = c = False
for i in range(n):
    if matrix[0][i] == 0:
        r = True  # 第一行需要归零
for i in range(m):
    if matrix[i][0] == 0:
        c = True  # 第一列需要归零
</code></pre>
<blockquote>
<p>⚠️ <strong>注意</strong>：<code>matrix[0][0]</code> 同时属于第一行和第一列。如果它是 0，<code>r</code> 和 <code>c</code> 都会变成 <code>True</code>，这是正确的——第一行和第一列都得清零。</p>
</blockquote>
<p><strong>第二步：遍历工作区，在标记牌上记录</strong></p>
<p>遍历 <code>[1, m) × [1, n)</code>，遇到 0 就在第一行和第一列打标记：</p>
<pre><code>for i in range(1, m):
    for j in range(1, n):
        if matrix[i][j] == 0:
            matrix[i][0] = 0  # 第 i 行需要清零 → 刻在第一列
            matrix[0][j] = 0  # 第 j 列需要清零 → 刻在第一行
</code></pre>
<p><strong>第三步：根据标记牌，在工作区执行置零</strong></p>
<p>再次遍历 <code>[1, m) × [1, n)</code>，看一行或一列是否被标记：</p>
<pre><code>for i in range(1, m):
    for j in range(1, n):
        if matrix[i][0] == 0 or matrix[0][j] == 0:
            matrix[i][j] = 0
</code></pre>
<blockquote>
<p>💡 这里有一个巧妙的"自动继承"：如果第一行原本的某个 <code>matrix[0][j]</code> 就是 0（比如示例 2 中的 <code>matrix[0][3]=0</code>），它不需要我们在第二步额外标记，在第三步中会<strong>自然地</strong>让第 j 列的工作区被置零——因为 <code>matrix[0][j]</code> 这个位置本身就同时是数据也是标记。</p>
</blockquote>
<p><strong>第四步：处理第一行和第一列自己</strong></p>
<p>最后才处理第一行和第一列——这必须在最后做，否则标记信息就丢了：</p>
<pre><code>if r:
    for i in range(n):
        matrix[0][i] = 0
if c:
    for i in range(m):
        matrix[i][0] = 0
</code></pre>
<h3>执行过程可视化</h3>
<p>以示例 1 <code>matrix = [[1,1,1],[1,0,1],[1,1,1]]</code> 为例：</p>
<p><strong>步骤 1：检查首行首列</strong></p>
<ul>
<li>第一行 <code>[1,1,1]</code>，无 0 → <code>r = False</code></li>
<li>第一列 <code>[1,1,1]</code>，无 0 → <code>c = False</code></li>
</ul>
<p><strong>步骤 2：打标记</strong>（遍历 <code>i=1,2</code> 和 <code>j=1,2</code>）</p>
<table>
<thead>
<tr>
<th><code>i</code></th>
<th><code>j</code></th>
<th><code>matrix[i][j]</code></th>
<th>操作</th>
<th>矩阵变化</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>1</td>
<td><strong>0</strong></td>
<td><code>matrix[1][0]=0</code>, <code>matrix[0][1]=0</code></td>
<td><code>[[1,**0**,1],[**0**,0,1],[1,1,1]]</code></td>
</tr>
<tr>
<td>1</td>
<td>2</td>
<td>1</td>
<td>无操作</td>
<td>不变</td>
</tr>
<tr>
<td>2</td>
<td>1</td>
<td>1</td>
<td>无操作</td>
<td>不变</td>
</tr>
<tr>
<td>2</td>
<td>2</td>
<td>1</td>
<td>无操作</td>
<td>不变</td>
</tr>
</tbody>
</table>
<p>标记完成后的矩阵：<code>[[1,0,1],[0,0,1],[1,1,1]]</code></p>
<p><strong>步骤 3：根据标记执行</strong>（遍历 <code>i=1,2</code> 和 <code>j=1,2</code>）</p>
<table>
<thead>
<tr>
<th><code>i</code></th>
<th><code>j</code></th>
<th><code>matrix[i][0]</code></th>
<th><code>matrix[0][j]</code></th>
<th>条件满足？</th>
<th>操作</th>
<th>矩阵变化</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>1</td>
<td><code>0</code></td>
<td><code>0</code></td>
<td>是</td>
<td>置零</td>
<td>已经是 0</td>
</tr>
<tr>
<td>1</td>
<td>2</td>
<td><code>0</code></td>
<td><code>1</code></td>
<td>是（<code>matrix[1][0]=0</code>）</td>
<td>置零</td>
<td><code>matrix[1][2]=0</code></td>
</tr>
<tr>
<td>2</td>
<td>1</td>
<td><code>1</code></td>
<td><code>0</code></td>
<td>是（<code>matrix[0][1]=0</code>）</td>
<td>置零</td>
<td><code>matrix[2][1]=0</code></td>
</tr>
<tr>
<td>2</td>
<td>2</td>
<td><code>1</code></td>
<td><code>1</code></td>
<td>否</td>
<td>不变</td>
<td>不变</td>
</tr>
</tbody>
</table>
<p><strong>步骤 4：处理首行首列</strong></p>
<ul>
<li><code>r = False</code> → 不动第一行</li>
<li><code>c = False</code> → 不动第一列</li>
</ul>
<p>最终结果：<code>[[1,0,1],[0,0,0],[1,0,1]]</code> ✅</p>
<hr />
<p>再看示例 2 <code>matrix = [[0,1,2,0],[3,4,5,2],[1,3,1,5]]</code>（$m=3, n=4$）：</p>
<p><strong>步骤 1：检查首行首列</strong></p>
<ul>
<li>第一行 <code>[0,1,2,0]</code>，有 0 → <code>r = True</code></li>
<li>第一列 <code>[0,3,1]</code>，有 0 → <code>c = True</code></li>
</ul>
<p><strong>步骤 2：打标记</strong>（遍历 <code>i=1,2</code> 和 <code>j=1,2,3</code>）</p>
<p>所有元素都非 0，不写入新标记。但注意——<code>matrix[0][3]</code> 原本就是 0，它作为"列 3 的标记"已经存在了！</p>
<p><strong>步骤 3：根据标记执行</strong>（遍历 <code>i=1,2</code> 和 <code>j=1,2,3</code>）</p>
<table>
<thead>
<tr>
<th><code>i</code></th>
<th><code>j</code></th>
<th><code>matrix[i][0]</code></th>
<th><code>matrix[0][j]</code></th>
<th>条件满足？</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>1</td>
<td><code>3</code></td>
<td><code>1</code></td>
<td>否</td>
<td>不变</td>
</tr>
<tr>
<td>1</td>
<td>2</td>
<td><code>3</code></td>
<td><code>2</code></td>
<td>否</td>
<td>不变</td>
</tr>
<tr>
<td>1</td>
<td>3</td>
<td><code>3</code></td>
<td><strong><code>0</code></strong></td>
<td>是</td>
<td><code>matrix[1][3]=0</code></td>
</tr>
<tr>
<td>2</td>
<td>1</td>
<td><code>1</code></td>
<td><code>1</code></td>
<td>否</td>
<td>不变</td>
</tr>
<tr>
<td>2</td>
<td>2</td>
<td><code>1</code></td>
<td><code>2</code></td>
<td>否</td>
<td>不变</td>
</tr>
<tr>
<td>2</td>
<td>3</td>
<td><code>1</code></td>
<td><strong><code>0</code></strong></td>
<td>是</td>
<td><code>matrix[2][3]=0</code></td>
</tr>
</tbody>
</table>
<p><strong>步骤 4：处理首行首列</strong></p>
<ul>
<li><code>r = True</code> → 第一行全置零</li>
<li><code>c = True</code> → 第一列全置零</li>
</ul>
<p>最终结果：<code>[[0,0,0,0],[0,4,5,0],[0,3,1,0]]</code> ✅</p>
<blockquote>
<p>💡 示例 2 中，<code>matrix[0][3]=0</code> 从未被步骤 2 刻意标记过——它<strong>天生就是标记</strong>。在步骤 3 中，任何工作区元素只要列号 j=3，就会发现 <code>matrix[0][3]=0</code>，从而被正确置零。第一行中不在第一列的 0，天然充当了该列的标记——这也是为什么步骤 1 要先检查第一行并记录 <code>r</code>：因为第一行自己的 0 既承担"这一列要清零"的标记职责，又承担"整行自身也需要清零"的数据职责。</p>
</blockquote>
<h2>复杂度</h2>
<ul>
<li><strong>时间复杂度</strong>：$O(mn)$。两遍遍历，每个元素被访问常数次。</li>
<li><strong>空间复杂度</strong>：$O(1)$。只使用了两个布尔变量 <code>r</code> 和 <code>c</code>。</li>
</ul>
<h1>代码</h1>
<pre><code>class Solution:
    def setZeroes(self, matrix: List[List[int]]) -&gt; None:
        """
        Do not return anything, modify matrix in-place instead.
        """
        m, n = len(matrix), len(matrix[0])
        r = c = False
        for i in range(n):
            if matrix[0][i] == 0:
                r = True
        for i in range(m):
            if matrix[i][0] == 0:
                c = True
        for i in range(1, m):
            for j in range(1, n):
                if matrix[i][j] == 0:
                    matrix[i][0] = matrix[0][j] = 0
        
        for i in range(1, m):
            for j in range(1, n):
                if matrix[i][0] == 0 or matrix[0][j] == 0:
                    matrix[i][j] = 0
        
        if r:
            for i in range(n):
                matrix[0][i] = 0
        if c:
            for i in range(m):
                matrix[i][0] = 0
</code></pre>
<p>逐行解读：</p>
<ol>
<li><strong><code>r = c = False</code></strong>：两个布尔哨兵，记录第一行和第一列的"身世"——它们自己是否包含 0。</li>
<li><strong>检查第一行</strong>：遍历 <code>matrix[0][i]</code>，只要有一个 0，<code>r</code> 就为 <code>True</code>。</li>
<li><strong>检查第一列</strong>：同理记录 <code>c</code>。</li>
<li><strong>打标记</strong>：双重循环遍历工作区 <code>[1,m) × [1,n)</code>，<code>matrix[i][0] = matrix[0][j] = 0</code> 利用 Python 的链式赋值，一步将行标记和列标记同时写入。</li>
<li><strong>执行置零</strong>：再次遍历工作区，检查所在行的首元素或所在列的首元素是否为 0。</li>
<li><strong>收尾第一行</strong>：如果 <code>r</code> 为真，将整行清空。</li>
<li><strong>收尾第一列</strong>：如果 <code>c</code> 为真，将整列清空。</li>
</ol>
<blockquote>
<p>⚠️ <strong>顺序至关重要</strong>：步骤 6 和 7 必须在步骤 4-5 之后。如果先清零第一行/列，标记信息就丢失了，工作区将无法正确判断哪些行列需要置零。</p>
</blockquote>
<h1>总结</h1>
<p>「矩阵置零」教会我们一个经典的空间优化技巧：<strong>用输入数据的"边缘"做标记区</strong>。</p>
<blockquote>
<p>🧠 <strong>核心收获</strong>：当需要 $O(m+n)$ 的标记空间而只有 $O(mn)$ 的矩阵可用时，考虑把矩阵的第一行和第一列"征收"为标记数组。代价是额外用两个变量保存第一行和第一列自身的原始信息。这种"借用输入数据做辅助存储"的思路在算法设计中反复出现——当你被要求 $O(1)$ 空间却需要"记忆"某些信息时，问问自己：输入数组的某些位置能不能被复用？</p>
</blockquote>
<p>同类题目推荐：</p>
<ul>
<li><strong><a href="https://leetcode.cn/problems/game-of-life/">289. 生命游戏</a></strong>：同样需要原地修改矩阵，用"状态编码"技巧在同一个格子里同时存储旧状态和新状态</li>
<li><strong><a href="https://leetcode.cn/problems/rotate-image/">48. 旋转图像</a></strong>：原地旋转矩阵，同样需要精心设计操作顺序</li>
</ul>
]]></content>
    <author><name>Hygen</name></author>
    <category term="hot100"/>
  </entry>
  <entry>
    <title>除自身以外数组的乘积</title>
    <link href="https://www.hygen.red/posts/hot100/05-%E6%99%AE%E9%80%9A%E6%95%B0%E7%BB%84/016%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/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/hot100/05-%E6%99%AE%E9%80%9A%E6%95%B0%E7%BB%84/016%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/</id>
    <published>2026-07-03T00:00:00.000Z</published>
    <updated>2026-07-03T00:00:00.000Z</updated>
    <summary>Leetcode-238</summary>
    <content type="html"><![CDATA[<h1>题目描述</h1>
<p><a href="https://leetcode.cn/problems/product-of-array-except-self/description/?envType=study-plan-v2&amp;envId=top-100-liked">题目链接</a></p>
<p>给你一个整数数组 <code>nums</code>，返回数组 <code>answer</code>，其中 <code>answer[i]</code> 等于 <code>nums</code> 中除 <code>nums[i]</code> 之外其余各元素的乘积。</p>
<p>题目数据<strong>保证</strong>数组 <code>nums</code> 之中任意元素的全部前缀元素和后缀元素的乘积都在 32 位整数范围内。</p>
<p><strong>请勿使用除法</strong>，且在 $O(n)$ 时间复杂度内完成此题。</p>
<h2>输入</h2>
<p>整数数组 <code>nums</code></p>
<h2>输出</h2>
<p>数组 <code>answer</code>，其中 <code>answer[i]</code> 等于除 <code>nums[i]</code> 外其余元素的乘积</p>
<h2>约束条件</h2>
<ul>
<li><code>2 &lt;= nums.length &lt;= 10^5</code></li>
<li><code>-30 &lt;= nums[i] &lt;= 30</code></li>
<li>保证数组 <code>nums</code> 之中任意元素的全部前缀元素和后缀元素的乘积都在 32 位整数范围内</li>
</ul>
<h2>进阶约束</h2>
<p>你可以在 $O(1)$ 的额外空间复杂度内完成这个题目吗？（出于对空间复杂度分析的目的，输出数组<strong>不被视为</strong>额外空间。）</p>
<h2>示例1</h2>
<pre><code>输入：nums = [1,2,3,4]
输出：[24,12,8,6]
</code></pre>
<h2>示例2</h2>
<pre><code>输入：nums = [-1,1,0,-3,3]
输出：[0,0,9,0,0]
</code></pre>
<h1>解题思路</h1>
<p>题目要求计算「除自己以外所有元素的乘积」——直观来想，对于位置 <code>i</code>，我们需要知道<strong>左边所有元素的乘积</strong>和<strong>右边所有元素的乘积</strong>，然后把它们乘起来。</p>
<blockquote>
<p>💡 <strong>核心直觉</strong>：<code>answer[i] = (nums[0] × ... × nums[i-1]) × (nums[i+1] × ... × nums[n-1])</code>，即「前缀积 × 后缀积」。如果我们能高效地获取每个位置的左乘积和右乘积，问题就迎刃而解了。</p>
</blockquote>
<h2>从暴力枚举出发</h2>
<p>最直接的做法：对于每个位置 <code>i</code>，遍历整个数组计算乘积，跳过自身：</p>
<pre><code>class Solution:
    def productExceptSelf(self, nums: List[int]) -&gt; List[int]:
        n = len(nums)
        ans = [1] * n

        for i in range(n):
            for j in range(n):
                if i != j:
                    ans[i] *= nums[j]

        return ans
</code></pre>
<p><strong>瓶颈分析</strong>：时间复杂度 $O(n^2)$，当 $n \le 10^5$ 时，$10^{10}$ 次操作远超时间限制。而且内部循环做了大量重复计算——对于相邻的位置 <code>i</code> 和 <code>i+1</code>，它们共享了大量乘数，但暴力法每次都从头乘起。</p>
<p><strong>优化方向</strong>：能否复用已经算过的乘积？如果我们提前算好每个位置的「左边所有数的乘积」和「右边所有数的乘积」，那么每个位置的结果就是两者相乘，只需 $O(1)$ 即可得到。</p>
<h2>左右前缀积数组</h2>
<p>正如上文分析，我们可以用两个数组分别记录前缀积和后缀积：</p>
<ul>
<li><code>left[i]</code>：<code>nums[0]</code> 到 <code>nums[i-1]</code> 的乘积（不含 <code>nums[i]</code>）</li>
<li><code>right[i]</code>：<code>nums[i+1]</code> 到 <code>nums[n-1]</code> 的乘积（不含 <code>nums[i]</code>）</li>
</ul>
<p>那么 <code>answer[i] = left[i] * right[i]</code>。</p>
<pre><code>class Solution:
    def productExceptSelf(self, nums: List[int]) -&gt; List[int]:
        n = len(nums)
        left = [1] * n
        right = [1] * n
        ans = [1] * n

        # 从左到右计算前缀积
        for i in range(1, n):
            left[i] = left[i - 1] * nums[i - 1]

        # 从右到左计算后缀积
        for i in range(n - 2, -1, -1):
            right[i] = right[i + 1] * nums[i + 1]

        # 合并
        for i in range(n):
            ans[i] = left[i] * right[i]

        return ans
</code></pre>
<ul>
<li>时间复杂度：$O(n)$，三次遍历</li>
<li>空间复杂度：$O(n)$，需要 <code>left</code> 和 <code>right</code> 两个额外数组</li>
</ul>
<p>虽然时间已经最优，但题目进阶要求提出：能否只用 $O(1)$ 额外空间？</p>
<h2>空间优化：用输出数组替代前缀数组</h2>
<p>观察上述代码，<code>left</code> 和 <code>ans</code> 计算时机可以合并——我们先用 <code>ans</code> 数组存储前缀积，然后在从右向左遍历时，用一个变量 <code>r</code> 跟踪后缀积，边乘边更新：</p>
<pre><code>class Solution:
    def productExceptSelf(self, nums: List[int]) -&gt; List[int]:
        n = len(nums)
        ans = [1] * n

        # 第一遍：ans[i] 存储 i 左边所有元素的乘积
        for i in range(1, n):
            ans[i] = ans[i - 1] * nums[i - 1]

        # 第二遍：从右向左，用 r 跟踪右边乘积，直接乘入 ans
        r = 1
        for i in range(n - 1, -1, -1):
            ans[i] *= r
            r *= nums[i]

        return ans
</code></pre>
<blockquote>
<p>💡 <strong>关键优化</strong>：<code>ans</code> 数组一开始存放前缀积，然后从右向左遍历时「就地」乘上后缀积，一举两得。由于输出数组不计入额外空间，我们实现了 $O(1)$ 额外空间。</p>
</blockquote>
<h2>代码优化：双指针一次遍历</h2>
<p>上面的解法需要两次独立的遍历。有没有办法在<strong>一次遍历中同时完成</strong>前后缀积的累积？</p>
<p>答案是可以的。我们维护两个变量 <code>l</code> 和 <code>r</code>，分别记录从左到右和从右到左的累积乘积。在每次迭代中，<code>l</code> 代表当前位置 <code>i</code> 的「左边乘积」，<code>r</code> 代表对称位置 <code>n-i-1</code> 的「右边乘积」——两边同时推进，互不干扰：</p>
<pre><code>class Solution:
    def productExceptSelf(self, nums: List[int]) -&gt; List[int]:
        n = len(nums)
        l = r = 1
        res = [1] * n
        for i, x in enumerate(nums):
            res[i] *= l
            res[n - i - 1] *= r
            l *= x
            r *= nums[n - i - 1]
        return res
</code></pre>
<p>逐行解读这段代码的执行逻辑：</p>
<ol>
<li><strong>初始化</strong>：<code>l = r = 1</code>，分别代表「从左到右」和「从右到左」的累积乘积。<code>res</code> 全为 <code>1</code> 作为初始值。</li>
<li><strong>循环体内</strong>：
<ul>
<li><code>res[i] *= l</code>：将 <code>i</code> 位置乘上它<strong>左边所有元素</strong>的累积积</li>
<li><code>res[n - i - 1] *= r</code>：将对称位置 <code>n-i-1</code> 乘上它<strong>右边所有元素</strong>的累积积</li>
<li><code>l *= x</code>：把当前元素 <code>x</code> 纳入左边的累积</li>
<li><code>r *= nums[n - i - 1]</code>：把对称位置的元素纳入右边的累积</li>
</ul>
</li>
<li>循环结束时，<code>res[i]</code> 恰好被乘上了「左边累积 × 右边累积」，即除自身之外的乘积。</li>
</ol>
<blockquote>
<p>🧠 <strong>直觉理解</strong>：想象你从数组两端同时出发。<code>l</code> 是你从左边一路捡起的元素的乘积，每到一个新位置，先把这个位置的结果乘上 <code>l</code>（这是「我左边的东西」），然后再把当前元素「装进」<code>l</code> 给后面的位置用。<code>r</code> 从右边做同样的事。两个方向在一次循环中交错推进，互不干扰，因为它们操作的是数组的两个不同位置。</p>
</blockquote>
<h3>执行过程可视化</h3>
<p>以 <code>nums = [1, 2, 3, 4]</code> 为例，追踪 <code>l</code>、<code>r</code> 和 <code>res</code> 的变化：</p>
<table>
<thead>
<tr>
<th><code>i</code></th>
<th><code>x</code></th>
<th><code>n-i-1</code></th>
<th>操作前 <code>res</code></th>
<th><code>l</code> (操作前)</th>
<th><code>r</code> (操作前)</th>
<th>操作后 <code>res</code></th>
<th><code>l</code> (操作后)</th>
<th><code>r</code> (操作后)</th>
</tr>
</thead>
<tbody>
<tr>
<td>—</td>
<td>—</td>
<td>—</td>
<td><code>[1, 1, 1, 1]</code></td>
<td><code>1</code></td>
<td><code>1</code></td>
<td>—</td>
<td>—</td>
<td>—</td>
</tr>
<tr>
<td>0</td>
<td><code>1</code></td>
<td>3</td>
<td><code>[1, 1, 1, 1]</code></td>
<td><code>1</code></td>
<td><code>1</code></td>
<td><code>[1, 1, 1, 1]</code></td>
<td><code>1</code></td>
<td><code>4</code></td>
</tr>
<tr>
<td>1</td>
<td><code>2</code></td>
<td>2</td>
<td><code>[1, 1, 1, 1]</code></td>
<td><code>1</code></td>
<td><code>4</code></td>
<td><code>[1, 1, 4, 1]</code></td>
<td><code>2</code></td>
<td><code>12</code></td>
</tr>
<tr>
<td>2</td>
<td><code>3</code></td>
<td>1</td>
<td><code>[1, 1, 4, 1]</code></td>
<td><code>2</code></td>
<td><code>12</code></td>
<td><code>[1, 12, 8, 1]</code></td>
<td><code>6</code></td>
<td><code>24</code></td>
</tr>
<tr>
<td>3</td>
<td><code>4</code></td>
<td>0</td>
<td><code>[1, 12, 8, 1]</code></td>
<td><code>6</code></td>
<td><code>24</code></td>
<td><code>[24, 12, 8, 6]</code></td>
<td><code>24</code></td>
<td><code>24</code></td>
</tr>
</tbody>
</table>
<p>最终 <code>res = [24, 12, 8, 6]</code>，正是答案。</p>
<blockquote>
<p>⚠️ <strong>注意</strong>：<code>l</code> 在当前位置被用来更新 <code>res[i]</code> <strong>之后</strong>才乘上 <code>x</code>。这个顺序很关键——确保 <code>l</code> 始终代表「不含当前元素的左边乘积」。<code>r</code> 同理。</p>
</blockquote>
<h3>为什么这种写法是对的？</h3>
<p>对于位置 <code>i</code>，在循环的某一轮中：</p>
<ul>
<li>当 <code>i</code> 作为「左指针」被处理时，<code>l</code> 包含了 <code>nums[0..i-1]</code> 的乘积，所以 <code>res[i]</code> 被正确地乘上了左边部分</li>
<li>当 <code>n-i-1</code> 等于 <code>i</code>（即左右指针相遇），<code>r</code> 包含了 <code>nums[i+1..n-1]</code> 的乘积，所以 <code>res[i]</code> 被正确地乘上了右边部分</li>
</ul>
<p>因此，当循环结束时，每个 <code>res[i]</code> 都恰好被乘上了它左边和右边的所有元素——不多不少。</p>
<h2>复杂度</h2>
<ul>
<li><strong>时间复杂度</strong>：$O(n)$，只需一次遍历。</li>
<li><strong>空间复杂度</strong>：$O(1)$（输出数组不计入额外空间），仅使用了 <code>l</code>、<code>r</code> 两个变量。</li>
</ul>
<p>$O(n)$ 时间已是最优——因为输出结果需要为 $n$ 个位置分别计算，至少需要遍历一次数组。</p>
<h1>代码优化</h1>
<h2>三种写法的对比</h2>
<table>
<thead>
<tr>
<th>对比项</th>
<th>左右数组法</th>
<th>两次遍历法</th>
<th>单次双指针法</th>
</tr>
</thead>
<tbody>
<tr>
<td>遍历次数</td>
<td>3 次</td>
<td>2 次</td>
<td>1 次</td>
</tr>
<tr>
<td>额外空间</td>
<td>$O(n)$</td>
<td>$O(1)$</td>
<td>$O(1)$</td>
</tr>
<tr>
<td>代码行数</td>
<td>~12 行</td>
<td>~11 行</td>
<td>~9 行</td>
</tr>
<tr>
<td>可读性</td>
<td>⭐⭐⭐⭐⭐（最直观）</td>
<td>⭐⭐⭐⭐</td>
<td>⭐⭐⭐（需理解双指针交错逻辑）</td>
</tr>
<tr>
<td>推荐场景</td>
<td>面试中先展示思路</td>
<td>面试中优化后的标准解</td>
<td>追求极简和 Pythonic</td>
</tr>
</tbody>
</table>
<p>三种写法的时间复杂度均为 $O(n)$，差异在于常数因子和代码风格。面试中建议从「左右数组法」讲起，展示清晰的思维递进过程，最后落地到「两次遍历法」或「单次双指针法」。</p>
<blockquote>
<p>💡 <strong>面试建议</strong>：面试官更看重你的<strong>思维过程</strong>而非最终代码。先说「我们需要前缀积和后缀积」，用左右数组法写出第一版，再逐步优化——这个过程本身就能展现你的问题分解和优化能力。</p>
</blockquote>
<h2>处理零值的情况</h2>
<p><code>nums</code> 中可能包含 <code>0</code>（如示例2 <code>[-1, 1, 0, -3, 3]</code>），但上述算法天然能正确处理——因为前缀积会正常累积 <code>0</code>，后缀积同理。唯一需要注意的是：如果题目不保证乘积在 32 位范围内（本题保证了），可能需要考虑溢出问题。</p>
<h1>总结</h1>
<p>「除自身以外数组的乘积」的核心在于将问题拆解为两个独立维度——<strong>前缀积</strong>和<strong>后缀积</strong>——然后合并。这个思维模式在数组问题中非常经典：</p>
<ul>
<li><strong>前缀和 / 前缀积</strong>：预处理每个位置「从开头到这里的累积信息」</li>
<li><strong>后缀和 / 后缀积</strong>：预处理每个位置「从这里到末尾的累积信息」</li>
<li><strong>合并</strong>：在每个位置组合前后信息得到答案</li>
</ul>
<blockquote>
<p>🧠 <strong>核心收获</strong>：当你遇到「除了某个元素之外的所有元素」这种问题时，第一反应就应该是——<strong>把这个元素从中间"挖掉"，然后分别看它的左边和右边</strong>。前缀 + 后缀的组合模式能帮你把看似需要 $O(n^2)$ 的问题降到 $O(n)$。</p>
</blockquote>
<p>解决本题后，推荐挑战以下延伸题目——它们都涉及「前缀 + 后缀」或「预处理 + 合并」的思想：</p>
<ul>
<li><strong><a href="https://leetcode.cn/problems/trapping-rain-water/">42. 接雨水</a></strong>：每个位置能接多少水取决于它左边的最大高度和右边的最大高度——正是前缀最大值 + 后缀最大值的组合</li>
<li><strong><a href="https://leetcode.cn/problems/candy/">135. 分发糖果</a></strong>：每个孩子分多少糖取决于他左边和右边的相对排名——两次遍历，分别满足左右约束</li>
<li><strong><a href="https://leetcode.cn/problems/maximum-product-subarray/">152. 乘积最大子数组</a></strong>：将前缀积的思想延伸到子数组问题，乘积的符号翻转带来了新的挑战</li>
</ul>
]]></content>
    <author><name>Hygen</name></author>
    <category term="hot100"/>
  </entry>
  <entry>
    <title>缺失的第一个正数</title>
    <link href="https://www.hygen.red/posts/hot100/05-%E6%99%AE%E9%80%9A%E6%95%B0%E7%BB%84/017%E7%BC%BA%E5%A4%B1%E7%9A%84%E7%AC%AC%E4%B8%80%E4%B8%AA%E6%AD%A3%E6%95%B0/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/hot100/05-%E6%99%AE%E9%80%9A%E6%95%B0%E7%BB%84/017%E7%BC%BA%E5%A4%B1%E7%9A%84%E7%AC%AC%E4%B8%80%E4%B8%AA%E6%AD%A3%E6%95%B0/</id>
    <published>2026-07-03T00:00:00.000Z</published>
    <updated>2026-07-03T00:00:00.000Z</updated>
    <summary>Leetcode-41</summary>
    <content type="html"><![CDATA[<h1>题目描述</h1>
<p><a href="https://leetcode.cn/problems/first-missing-positive/description/?envType=study-plan-v2&amp;envId=top-100-liked">题目链接</a></p>
<p>给你一个未排序的整数数组 <code>nums</code>，请你找出其中没有出现的最小的正整数。</p>
<p>请你实现时间复杂度为 $O(n)$ 并且只使用常数级别额外空间的解决方案。</p>
<h2>输入</h2>
<p>未排序的整数数组 <code>nums</code></p>
<h2>输出</h2>
<p>没有出现的最小正整数</p>
<h2>约束条件</h2>
<ul>
<li><code>1 &lt;= nums.length &lt;= 10^5</code></li>
<li><code>-2^31 &lt;= nums[i] &lt;= 2^31 - 1</code></li>
</ul>
<h2>进阶约束</h2>
<p>实现时间复杂度 $O(n)$ 且空间复杂度 $O(1)$ 的解法。</p>
<h2>示例1</h2>
<pre><code>输入：nums = [1,2,0]
输出：3
解释：范围 [1,2] 中的数字都在数组中，因此缺失的最小正整数是 3。
</code></pre>
<h2>示例2</h2>
<pre><code>输入：nums = [3,4,-1,1]
输出：2
解释：1 在数组中，但 2 缺失，因此答案是 2。
</code></pre>
<h2>示例3</h2>
<pre><code>输入：nums = [7,8,9,11,12]
输出：1
解释：最小的正整数 1 缺失，因此答案是 1。
</code></pre>
<h1>解题思路</h1>
<p>题目要求找「缺失的第一个正整数」——对于长度为 $n$ 的数组，答案一定在 $[1, n+1]$ 这个范围内。为什么？因为：</p>
<ul>
<li>最坏情况下，数组恰好包含 $1$ 到 $n$ 这 $n$ 个正整数，那么缺失的第一个正整数就是 $n+1$</li>
<li>如果数组中缺失了 $[1, n]$ 中的某个数，答案就是那个缺失的数</li>
<li>数组中 ≤ 0 或 &gt; n 的数可以被<strong>忽略</strong>——因为它们不可能成为答案，反而占据了空间</li>
</ul>
<blockquote>
<p>💡 <strong>核心直觉</strong>：我们只需要关心 $[1, n]$ 范围内的数。如果能把每一个 $x \in [1, n]$ "放到它该在的位置"（即下标 $x-1$ 处），那么第一个"位置不对"的索引就揭示了缺失的正整数。</p>
</blockquote>
<h2>从暴力枚举出发</h2>
<p>最朴素的想法：从 $1$ 开始逐个检查每个正整数是否在数组中：</p>
<pre><code>class Solution:
    def firstMissingPositive(self, nums: List[int]) -&gt; int:
        s = set(nums)
        x = 1
        while x in s:
            x += 1
        return x
</code></pre>
<ul>
<li>时间复杂度：$O(n)$（建集合 $O(n)$，检查最多 $n+1$ 次，每次 $O(1)$）</li>
<li>空间复杂度：$O(n)$（需要一个哈希集合）</li>
</ul>
<p>虽然时间复杂度已是最优，但空间复杂度为 $O(n)$，不满足进阶要求。有没有办法把空间降到 $O(1)$？</p>
<p><strong>瓶颈分析</strong>：暴力法额外建了一个哈希集合来支持快速查找。如果能<strong>原地</strong>利用数组本身作为"哈希表"，就不需要额外空间了。</p>
<h2>原地哈希：让数组自己做"集合"</h2>
<p>关键的洞察是：数组的下标天然就是连续的整数 $0, 1, 2, \dots, n-1$。如果我们能让数组满足 <code>nums[i] = i + 1</code>（即下标 <code>i</code> 处存放值 <code>i+1</code>），那么数组本身就隐含了一个"集合"——要判断 $x$ 是否出现，只需要看 <code>nums[x-1]</code> 是否等于 $x$。</p>
<blockquote>
<p>🧠 <strong>直觉理解</strong>：想象你在整理一排有编号的储物柜。你有一堆写着号码的包裹，你希望每个包裹 <code>x</code> 被放进编号为 <code>x</code> 的柜子里。如果你按照这个规则逐个整理包裹，那么最终"柜子是空的"或"柜子里放的号码不对"的位置，就告诉你哪个正整数缺席了。</p>
</blockquote>
<p>具体来说，分两步走：</p>
<p><strong>第一步：重新摆放（交换）</strong></p>
<p>遍历数组，对于每个位置 <code>i</code>，如果 <code>nums[i]</code> 在 $[1, n]$ 范围内，并且它不在"正确的"位置上（即 <code>nums[i] != nums[nums[i] - 1]</code>），就把它和它"该去的位置"上的元素交换。重复这个过程直到当前位置无法再交换为止。</p>
<p>这里用 <code>while</code> 而不是 <code>if</code> 很关键——交换过来的新元素可能也在 $[1, n]$ 范围内，需要继续为它寻找正确位置：</p>
<pre><code>for i in range(n):
    while 1 &lt;= nums[i] &lt;= n and nums[i] != nums[nums[i] - 1]:
        nums[nums[i] - 1], nums[i] = nums[i], nums[nums[i] - 1]
</code></pre>
<p><strong>第二步：寻找缺失</strong></p>
<p>交换完成后，再遍历一次数组。第一个不满足 <code>nums[i] == i + 1</code> 的位置就对应着缺失的正整数：</p>
<pre><code>for i in range(n):
    if nums[i] != i + 1:
        return i + 1
return n + 1
</code></pre>
<h3>执行过程可视化</h3>
<p>以 <code>nums = [3, 4, -1, 1]</code>（$n=4$）为例，追踪交换过程：</p>
<table>
<thead>
<tr>
<th>轮次</th>
<th><code>i</code></th>
<th>交换前数组</th>
<th><code>nums[i]</code></th>
<th>目标位置</th>
<th>交换后数组</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>—</td>
<td>—</td>
<td>—</td>
<td>—</td>
<td>—</td>
<td><code>[3, 4, -1, 1]</code></td>
<td>初始状态</td>
</tr>
<tr>
<td>1</td>
<td>0</td>
<td><code>[3, 4, -1, 1]</code></td>
<td><code>3</code></td>
<td><code>nums[2]</code></td>
<td><code>[-1, 4, 3, 1]</code></td>
<td><code>3</code> 和 <code>nums[2] = -1</code> 交换</td>
</tr>
<tr>
<td>1</td>
<td>0</td>
<td><code>[-1, 4, 3, 1]</code></td>
<td><code>-1</code></td>
<td>—</td>
<td>不变</td>
<td>-1 不在 [1,4] 内，停止</td>
</tr>
<tr>
<td>2</td>
<td>1</td>
<td><code>[-1, 4, 3, 1]</code></td>
<td><code>4</code></td>
<td><code>nums[3]</code></td>
<td><code>[-1, 1, 3, 4]</code></td>
<td><code>4</code> 和 <code>nums[3] = 1</code> 交换</td>
</tr>
<tr>
<td>2</td>
<td>1</td>
<td><code>[-1, 1, 3, 4]</code></td>
<td><code>1</code></td>
<td><code>nums[0]</code></td>
<td><code>[1, -1, 3, 4]</code></td>
<td><code>1</code> 和 <code>nums[0] = -1</code> 交换</td>
</tr>
<tr>
<td>2</td>
<td>1</td>
<td><code>[1, -1, 3, 4]</code></td>
<td><code>-1</code></td>
<td>—</td>
<td>不变</td>
<td>-1 不在 [1,4] 内，停止</td>
</tr>
<tr>
<td>3</td>
<td>2</td>
<td><code>[1, -1, 3, 4]</code></td>
<td><code>3</code></td>
<td><code>nums[2]</code></td>
<td>不变</td>
<td><code>nums[2]</code> 已经是 3，跳过</td>
</tr>
<tr>
<td>4</td>
<td>3</td>
<td><code>[1, -1, 3, 4]</code></td>
<td><code>4</code></td>
<td><code>nums[3]</code></td>
<td>不变</td>
<td><code>nums[3]</code> 已经是 4，跳过</td>
</tr>
</tbody>
</table>
<p>交换结束后，数组变为 <code>[1, -1, 3, 4]</code>。遍历检查：</p>
<ul>
<li><code>i=0</code>: <code>nums[0]=1</code> ✅</li>
<li><code>i=1</code>: <code>nums[1]=-1 ≠ 2</code> ❌ → 返回 <code>2</code></li>
</ul>
<p>答案正是 <code>2</code>。</p>
<blockquote>
<p>⚠️ <strong>注意</strong>：交换条件中 <code>nums[i] != nums[nums[i] - 1]</code> 不能写成 <code>nums[i] != i + 1</code>。考虑 <code>[1, 1]</code> 这种情况——<code>nums[0]=1</code> 已经在正确位置，但 <code>nums[1]=1</code> 与 <code>nums[1-1]=nums[0]=1</code> 相同，如果只用 <code>nums[i] != i + 1</code> 作为条件会陷入死循环（一直在原地交换相同的两个 1）。</p>
</blockquote>
<h2>复杂度</h2>
<ul>
<li><strong>时间复杂度</strong>：$O(n)$。虽然内层有 <code>while</code> 循环，但每个数最多被交换一次到正确位置，总交换次数不超过 $n$ 次。</li>
<li><strong>空间复杂度</strong>：$O(1)$。只使用了常数个临时变量，在原地修改数组。</li>
</ul>
<blockquote>
<p>💡 <strong>为什么时间复杂度不是 $O(n^2)$？</strong> 外层循环 $n$ 次，内层的 <code>while</code> 看似可以多轮交换，但每次交换至少让一个数去到它的最终位置。每个数一旦"落户"就不会再被移动（因为后续 <code>nums[i] != nums[nums[i]-1]</code> 条件不成立）。因此总交换操作 ≤ $n$ 次，整个算法仍是线性的。这种分析方法叫做<strong>均摊分析</strong>。</p>
</blockquote>
<h1>解题思路（标记法）</h1>
<p>除了交换法，还有一种原地哈希的实现——用正负号做标记。</p>
<p><strong>核心思想</strong>：第一遍遍历，对于每个 $x \in [1, n]$，将 <code>nums[x-1]</code> 标记为负数，表示"$x$ 出现过"。第二遍遍历，第一个正数的位置就对应缺失的正整数。</p>
<pre><code>class Solution:
    def firstMissingPositive(self, nums: List[int]) -&gt; int:
        n = len(nums)

        # 第一步：将非正数设为 n+1（不影响后续标记）
        for i in range(n):
            if nums[i] &lt;= 0:
                nums[i] = n + 1

        # 第二步：用负号做标记
        for i in range(n):
            x = abs(nums[i])
            if 1 &lt;= x &lt;= n and nums[x - 1] &gt; 0:
                nums[x - 1] = -nums[x - 1]

        # 第三步：找第一个正数
        for i in range(n):
            if nums[i] &gt; 0:
                return i + 1

        return n + 1
</code></pre>
<p>这个方法同样满足 $O(n)$ 时间和 $O(1)$ 空间，但它修改了数组中元素的符号，且需要恢复或接受破坏。相比而言，交换法更直观——数组的最终状态就是"整理好的柜子"，不需要额外的"负号"语义。</p>
<table>
<thead>
<tr>
<th>对比项</th>
<th>交换法</th>
<th>标记法</th>
</tr>
</thead>
<tbody>
<tr>
<td>核心操作</td>
<td>交换元素到正确位置</td>
<td>用负号标记出现过的数</td>
</tr>
<tr>
<td>数组最终状态</td>
<td>部分有序（<code>nums[i]=i+1</code> 或不在范围内的数）</td>
<td>元素可能被取反，需要 <code>abs()</code> 恢复</td>
</tr>
<tr>
<td>可读性</td>
<td>⭐⭐⭐⭐</td>
<td>⭐⭐⭐</td>
</tr>
<tr>
<td>稳定性</td>
<td>不改变元素绝对值</td>
<td>改变了元素符号</td>
</tr>
</tbody>
</table>
<h1>代码</h1>
<h2>最终代码（交换法）</h2>
<pre><code>class Solution:
    def firstMissingPositive(self, nums: List[int]) -&gt; int:
        n = len(nums)
        for i in range(n):
            while 1 &lt;= nums[i] &lt;= n and nums[i] != nums[nums[i] - 1]:
                nums[nums[i] - 1], nums[i] = nums[i], nums[nums[i] - 1]
        for i in range(n):
            if nums[i] != i + 1:
                return i + 1
        return n + 1
</code></pre>
<p>逐行解读：</p>
<ol>
<li><strong><code>n = len(nums)</code></strong>：数组长度。答案的范围锁定在 $[1, n+1]$。</li>
<li><strong>外层循环 <code>for i in range(n)</code></strong>：遍历每个位置，尝试让该位置存放正确的数。</li>
<li><strong>交换条件 <code>while 1 &lt;= nums[i] &lt;= n and nums[i] != nums[nums[i] - 1]</code></strong>：
<ul>
<li><code>1 &lt;= nums[i] &lt;= n</code>：只关心 $[1, n]$ 范围内的数，非正数和大于 $n$ 的数可以留在原地</li>
<li><code>nums[i] != nums[nums[i] - 1]</code>：当前数不在它该在的位置上（避免重复交换导致死循环）</li>
</ul>
</li>
<li><strong>交换</strong>：<code>nums[nums[i] - 1], nums[i] = nums[i], nums[nums[i] - 1]</code>——Python 的元组解包一次性完成交换，不需要临时变量</li>
<li><strong>第二遍扫描</strong>：第一个 <code>nums[i] != i + 1</code> 的位置，说明 <code>i+1</code> 缺失，直接返回</li>
<li><strong>兜底 <code>return n + 1</code></strong>：所有位置都正确，说明 $[1, n]$ 全部出现，返回 $n+1$</li>
</ol>
<blockquote>
<p>⚠️ <strong>交换语句的顺序要留意</strong>：<code>nums[nums[i] - 1], nums[i] = nums[i], nums[nums[i] - 1]</code> 是正确的。如果写成 <code>nums[i], nums[nums[i] - 1] = nums[nums[i] - 1], nums[i]</code>，Python 会先计算右侧，再从左到右赋值——这时 <code>nums[i]</code> 先被修改，后续 <code>nums[nums[i] - 1]</code> 中的 <code>nums[i]</code> 已经变了，导致赋值到错误的位置。所以务必把 <code>nums[nums[i] - 1]</code> 放在赋值语句的左侧第一位。</p>
</blockquote>
<h1>总结</h1>
<p>「缺失的第一个正数」教会我们一个重要的思维：<strong>将数组的索引视为哈希表的键</strong>。</p>
<blockquote>
<p>🧠 <strong>核心收获</strong>：当你需要在 $O(1)$ 空间内做「存在性检查」时，考虑<strong>原地哈希</strong>——把数组本身当哈希表用。前提是你能建立「元素值 → 数组下标」的映射关系。本题中映射关系是 $x \to x-1$，恰到好处。</p>
</blockquote>
<p>这个技巧在很多题目中都有类似的应用：</p>
<ul>
<li><strong><a href="https://leetcode.cn/problems/find-all-duplicates-in-an-array/">442. 数组中重复的数据</a></strong>：同样是 $[1, n]$ 范围的整数，同样可以用原地交换或正负标记找出重复项</li>
<li><strong><a href="https://leetcode.cn/problems/find-all-numbers-disappeared-in-an-array/">448. 找到所有数组中消失的数字</a></strong>：本题的"兄弟题"，找的是所有缺失的数而非第一个</li>
<li><strong><a href="https://leetcode.cn/problems/find-the-duplicate-number/">287. 寻找重复数</a></strong>：利用下标映射 + 快慢指针，将数组问题转化为链表环检测问题</li>
</ul>
]]></content>
    <author><name>Hygen</name></author>
    <category term="hot100"/>
  </entry>
  <entry>
    <title>穿越网格图的安全路径</title>
    <link href="https://www.hygen.red/posts/dsa/solution/015%E7%A9%BF%E8%B6%8A%E7%BD%91%E6%A0%BC%E5%9B%BE%E7%9A%84%E5%AE%89%E5%85%A8%E8%B7%AF%E5%BE%84/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/dsa/solution/015%E7%A9%BF%E8%B6%8A%E7%BD%91%E6%A0%BC%E5%9B%BE%E7%9A%84%E5%AE%89%E5%85%A8%E8%B7%AF%E5%BE%84/</id>
    <published>2026-07-02T00:00:00.000Z</published>
    <updated>2026-07-02T00:00:00.000Z</updated>
    <summary>LeetCode-3286</summary>
    <content type="html"><![CDATA[<h1>题目描述</h1>
<p><a href="https://leetcode.cn/problems/find-a-safe-walk-through-a-grid/description/?envType=daily-question&amp;envId=2026-07-02">题目链接</a></p>
<p>给你一个 $m \times n$ 的二维二进制数组 <code>grid</code> 和一个整数 <code>health</code>。</p>
<p>你从左上角 $(0, 0)$ 出发，目标是到达右下角 $(m-1, n-1)$。每次可以向<strong>上、下、左、右</strong>四个方向之一移动。</p>
<ul>
<li>经过 <code>grid[i][j] = 0</code> 的格子是安全的，<strong>不消耗</strong>健康值；</li>
<li>经过 <code>grid[i][j] = 1</code> 的格子是危险的，<strong>消耗 1 点</strong>健康值。</li>
</ul>
<p>返回是否能在<strong>健康值始终为正</strong>（即到达终点时健康值 $\ge 1$）的前提下到达终点。</p>
<h2>输入</h2>
<ul>
<li><code>grid</code>：$m \times n$ 的二进制矩阵（元素值为 <code>0</code> 或 <code>1</code>）</li>
<li><code>health</code>：初始健康值（整数）</li>
</ul>
<h2>输出</h2>
<ul>
<li><code>bool</code>：能否安全到达终点</li>
</ul>
<h2>约束条件</h2>
<ol>
<li><code>m == grid.length</code></li>
<li><code>n == grid[i].length</code></li>
<li><code>1 &lt;= m, n &lt;= 50</code></li>
<li><code>2 &lt;= m * n</code></li>
<li><code>1 &lt;= health &lt;= m + n</code></li>
<li><code>grid[i][j]</code> 为 <code>0</code> 或 <code>1</code></li>
</ol>
<h2>示例1</h2>
<pre><code>输入: grid = [[0,1,0,0,0],[0,1,0,1,0],[0,0,0,1,0]], health = 1
输出: true
</code></pre>
<p>解释：可以沿着如下路径（沿值为 <code>0</code> 的格子）安全到达终点，不消耗任何健康值，终点时健康值仍为 <code>1</code>。</p>
<h2>示例2</h2>
<pre><code>输入: grid = [[0,1,1,0,0,0],[1,0,1,0,0,0],[0,1,1,1,0,1],[0,0,1,0,1,0]], health = 3
输出: false
</code></pre>
<p>解释：从起点到终点至少需要消耗 <code>4</code> 点健康值，初始 <code>health = 3</code> 不足以到达。</p>
<h2>示例3</h2>
<pre><code>输入: grid = [[1,1,1],[1,0,1],[1,1,1]], health = 5
输出: true
</code></pre>
<p>解释：经过中间的安全格子 <code>(1, 1)</code>，累计消耗为 <code>4</code>，终点时健康值为 <code>5 - 4 = 1</code>，满足条件。</p>
<h1>解题思路</h1>
<p>问题本质是：在网格中寻找一条从起点到终点的路径，使得路径上所有格子的值之和（总消耗）<strong>严格小于</strong>初始健康值。这是一个典型的<strong>最短路径</strong>问题——每条边的权重就是目标格子的值（<code>0</code> 或 <code>1</code>），我们要找累计权重最小的路径。</p>
<blockquote>
<p>🧠 <strong>直觉理解</strong>：把 <code>grid</code> 想象成一片沼泽地——值为 <code>0</code> 的格子是硬路面（不费体力），值为 <code>1</code> 的格子是泥潭（每踩一脚耗 1 点体力）。问题等价于：从左上走到右下，能否在体力耗尽前到达？</p>
</blockquote>
<h2>从暴力搜索出发</h2>
<p>最直接的想法：用 DFS 枚举所有可能的路径，沿途累加消耗，到达终点时检查是否小于 <code>health</code>：</p>
<pre><code>class Solution:
    def findSafeWalk(self, grid: List[List[int]], health: int) -&gt; bool:
        m, n = len(grid), len(grid[0])
        visited = [[False] * n for _ in range(m)]

        def dfs(x: int, y: int, cost: int) -&gt; bool:
            if x == m - 1 and y == n - 1:
                return cost &lt; health
            visited[x][y] = True
            for a, b in (x, y + 1), (x, y - 1), (x + 1, y), (x - 1, y):
                if 0 &lt;= a &lt; m and 0 &lt;= b &lt; n and not visited[a][b]:
                    new_cost = cost + grid[a][b]
                    if new_cost &lt; health:  # 剪枝：已经超过 health 就不必继续
                        if dfs(a, b, new_cost):
                            return True
            visited[x][y] = False
            return False

        return dfs(0, 0, grid[0][0])
</code></pre>
<p>这种做法的时间复杂度是指数级的——每个格子都可能被多次访问，最坏情况下需要遍历所有 $4^{mn}$ 条可能的路径。在 $m, n \le 50$ 时完全不可行。但暴力解揭示了一件重要的事：<strong>我们只关心到达每个格子的「最小累计消耗」，而不是路径本身</strong>。</p>
<h2>建模为最短路径问题</h2>
<p>将每个格子视为图中的节点，相邻格子的移动视为有向边，边的权重等于<strong>目标格子</strong>的值 <code>grid[a][b]</code>。那么「到达终点的最小健康消耗」就是从起点 $(0, 0)$ 到终点 $(m-1, n-1)$ 的<strong>最短路径权重</strong>。</p>
<p>因为边权只有 <code>0</code>（安全格子）和 <code>1</code>（危险格子）两种，经典的 <strong>0-1 BFS</strong> 正好适用。</p>
<blockquote>
<p>💡 <strong>小贴士</strong>：0-1 BFS 是 Dijkstra 算法在边权仅为 0 或 1 时的特化版本——用双端队列（deque）代替优先队列，将时间复杂度从 $O(V \log V)$ 优化到 $O(V)$。核心技巧：遇到权重 <code>0</code> 的边，将新节点插入队<strong>首</strong>（优先处理）；遇到权重 <code>1</code> 的边，插入队<strong>尾</strong>。</p>
</blockquote>
<h2>0-1 BFS 推演</h2>
<p>维护一个距离矩阵 <code>d[i][j]</code>，表示从起点 $(0, 0)$ 到 $(i, j)$ 的最小累计消耗，初始化为无穷大，起点处 <code>d[0][0] = grid[0][0]</code>。</p>
<p>每次从 deque 前端取出一个格子 $(x, y)$，检查它的四个邻居 $(a, b)$：</p>
<ul>
<li>如果通过 $(x, y)$ 到达 $(a, b)$ 的消耗 $d[x][y] + grid[a][b]$ 比已知的 $d[a][b]$ 更小，则更新 $d[a][b]$</li>
<li>若 <code>grid[a][b] == 0</code>（零消耗边），插入队首 → 下轮优先处理，确保同等消耗的格子被连续探索</li>
<li>若 <code>grid[a][b] == 1</code>（单位消耗边），插入队尾 → 等当前消耗层级处理完后再处理</li>
</ul>
<blockquote>
<p>⚠️ <strong>注意</strong>：0-1 BFS 要求从 deque <strong>前端</strong>取出节点（<code>popleft</code>），因为 <code>appendleft</code> 会打乱 BFS 层级顺序。这个细节决定了算法正确性——如果错误地从后端取出，就退化成了普通队列 BFS，无法保证最优。</p>
</blockquote>
<p>以示例 1 的网格为例追踪算法执行（<code>health = 1</code>）：</p>
<pre><code>grid:
[0, 1, 0, 0, 0]
[0, 1, 0, 1, 0]
[0, 0, 0, 1, 0]
</code></pre>
<table>
<thead>
<tr>
<th>步骤</th>
<th>出队 <code>(x,y)</code></th>
<th><code>d[x][y]</code></th>
<th>更新的邻居</th>
<th>新 <code>d</code> 值</th>
<th>入队位置</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>—</td>
<td>—</td>
<td>初始化</td>
<td><code>d[0][0]=0</code></td>
<td>队尾 <code>(0,0)</code></td>
</tr>
<tr>
<td>1</td>
<td><code>(0,0)</code></td>
<td>0</td>
<td><code>(1,0)</code> cost=0</td>
<td><code>d[1][0]=0</code></td>
<td>队首</td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td><code>(0,1)</code> cost=1</td>
<td><code>d[0][1]=1</code></td>
<td>队尾</td>
</tr>
<tr>
<td>2</td>
<td><code>(1,0)</code></td>
<td>0</td>
<td><code>(2,0)</code> cost=0</td>
<td><code>d[2][0]=0</code></td>
<td>队首</td>
</tr>
<tr>
<td>3</td>
<td><code>(2,0)</code></td>
<td>0</td>
<td><code>(2,1)</code> cost=0</td>
<td><code>d[2][1]=0</code></td>
<td>队首</td>
</tr>
<tr>
<td>4</td>
<td><code>(2,1)</code></td>
<td>0</td>
<td><code>(2,2)</code> cost=0</td>
<td><code>d[2][2]=0</code></td>
<td>队首</td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td><code>(1,1)</code> cost=1</td>
<td><code>d[1][1]=1</code></td>
<td>队尾</td>
</tr>
<tr>
<td>5</td>
<td><code>(2,2)</code></td>
<td>0</td>
<td><code>(2,3)</code> cost=1</td>
<td><code>d[2][3]=1</code></td>
<td>队尾</td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td><code>(1,2)</code> cost=0</td>
<td><code>d[1][2]=0</code></td>
<td>队首</td>
</tr>
<tr>
<td>6</td>
<td><code>(1,2)</code></td>
<td>0</td>
<td><code>(0,2)</code> cost=0</td>
<td><code>d[0][2]=0</code></td>
<td>队首</td>
</tr>
<tr>
<td>7</td>
<td><code>(0,2)</code></td>
<td>0</td>
<td><code>(0,3)</code> cost=0</td>
<td><code>d[0][3]=0</code></td>
<td>队首</td>
</tr>
<tr>
<td>8</td>
<td><code>(0,3)</code></td>
<td>0</td>
<td><code>(0,4)</code> cost=0</td>
<td><code>d[0][4]=0</code></td>
<td>队首</td>
</tr>
<tr>
<td>9</td>
<td><code>(0,4)</code></td>
<td>0</td>
<td><code>(1,4)</code> cost=0</td>
<td><code>d[1][4]=0</code></td>
<td>队首</td>
</tr>
<tr>
<td>10</td>
<td><code>(1,4)</code></td>
<td>0</td>
<td><code>(2,4)</code> cost=0</td>
<td><code>d[2][4]=0</code></td>
<td>队首</td>
</tr>
</tbody>
</table>
<p>最终 <code>d[2][4] = 0 &lt; health = 1</code>，返回 <code>True</code>。整个过程队列始终优先处理 <code>cost=0</code> 的邻居，确保找到的路径是最优的。</p>
<h2>复杂度</h2>
<ul>
<li>时间复杂度：$O(m \times n)$，每个格子最多入队一次（因为只在发现更优消耗时更新并入队，而 <code>d</code> 值的范围极小——每个格子只会被更新 $O(1)$ 次）</li>
<li>空间复杂度：$O(m \times n)$，距离矩阵 <code>d</code> 和 deque 的最大长度</li>
</ul>
<h2>代码</h2>
<pre><code>from collections import deque
from math import inf
from typing import List


class Solution:
    def findSafeWalk(self, grid: List[List[int]], health: int) -&gt; bool:
        m, n = len(grid), len(grid[0])
        d = [[inf] * n for _ in range(m)]
        d[0][0] = grid[0][0]

        q = deque([(0, 0)])
        while q:
            x, y = q.popleft()
            for a, b in (x, y + 1), (x, y - 1), (x + 1, y), (x - 1, y):
                if 0 &lt;= a &lt; m and 0 &lt;= b &lt; n:
                    if d[x][y] + grid[a][b] &lt; d[a][b]:
                        d[a][b] = d[x][y] + grid[a][b]
                        if not grid[a][b]:
                            q.appendleft((a, b))
                        else:
                            q.append((a, b))
        return d[-1][-1] &lt; health
</code></pre>
<p>代码的几个关键点：</p>
<ul>
<li><code>d[i][j]</code> 初始化为 <code>inf</code>，确保任何真实路径都能首次更新它</li>
<li><code>d[0][0] = grid[0][0]</code>：起点的格子值也算入消耗</li>
<li><code>grid[a][b]</code> 为 <code>0</code> 时 <code>appendleft</code>（优先处理），为 <code>1</code> 时 <code>append</code>（延后处理）</li>
<li>最终判断 <code>d[-1][-1] &lt; health</code>：消耗必须<strong>严格小于</strong>初始健康值，因为到达终点时健康值需 $\ge 1$</li>
</ul>
<h1>解题思路（Dijkstra）</h1>
<p>如果网格中的值不是仅限 <code>0/1</code> 而是任意非负整数，0-1 BFS 就无法直接使用了。此时需要用通用的 Dijkstra 算法——用小顶堆（优先队列）维护「当前已知的到各格子的最短距离」，每次取出距离最小的节点进行松弛。</p>
<pre><code>from heapq import heappop, heappush
from math import inf


class Solution:
    def findSafeWalk(self, grid: List[List[int]], health: int) -&gt; bool:
        m, n = len(grid), len(grid[0])
        d = [[inf] * n for _ in range(m)]
        d[0][0] = grid[0][0]

        pq = [(d[0][0], 0, 0)]  # (cost, x, y)
        while pq:
            cost, x, y = heappop(pq)
            if cost &gt; d[x][y]:
                continue  # 过期数据，跳过
            if x == m - 1 and y == n - 1:
                return cost &lt; health
            for a, b in (x, y + 1), (x, y - 1), (x + 1, y), (x - 1, y):
                if 0 &lt;= a &lt; m and 0 &lt;= b &lt; n:
                    new_cost = cost + grid[a][b]
                    if new_cost &lt; d[a][b]:
                        d[a][b] = new_cost
                        heappush(pq, (new_cost, a, b))
        return False
</code></pre>
<table>
<thead>
<tr>
<th>对比项</th>
<th>0-1 BFS</th>
<th>Dijkstra</th>
</tr>
</thead>
<tbody>
<tr>
<td>时间</td>
<td>$O(mn)$</td>
<td>$O(mn \log(mn))$</td>
</tr>
<tr>
<td>空间</td>
<td>$O(mn)$</td>
<td>$O(mn)$</td>
</tr>
<tr>
<td>数据结构</td>
<td>双端队列 <code>deque</code></td>
<td>小顶堆 <code>heapq</code></td>
</tr>
<tr>
<td>适用场景</td>
<td>边权仅 $0$ 或 $1$</td>
<td>边权为任意非负整数</td>
</tr>
<tr>
<td>代码简洁度</td>
<td>稍简洁</td>
<td>也较简洁</td>
</tr>
</tbody>
</table>
<p>本题 <code>grid</code> 的值只有 <code>0</code> 和 <code>1</code>，0-1 BFS 是最优选择。Dijkstra 也能过（$50 \times 50 = 2500$ 个节点，$\log$ 开销很小），但面试中展示 0-1 BFS 能体现对边权特殊性质的洞察。</p>
<h1>代码优化</h1>
<p>0-1 BFS 已经是本题的最优算法，以下聚焦于工程细节。</p>
<h2>提前返回</h2>
<p>一旦从 deque 中取出的节点是终点 <code>(m-1, n-1)</code>，<code>d[m-1][n-1]</code> 已经是最小消耗——因为 0-1 BFS 保证节点首次出队时 <code>d</code> 值即为最短距离（类似 Dijkstra）。可以直接返回而不必跑完整个 BFS：</p>
<pre><code>while q:
    x, y = q.popleft()
    if x == m - 1 and y == n - 1:
        return d[x][y] &lt; health
    for a, b in (x, y + 1), (x, y - 1), (x + 1, y), (x - 1, y):
        ...
</code></pre>
<h2>方向数组提取</h2>
<p>将四个方向的偏移量提取为常量，减少 for 循环中的元组构造开销（微优化，但对竞赛场景有意义）：</p>
<pre><code>DIRS = [(0, 1), (0, -1), (1, 0), (-1, 0)]
...
for dx, dy in DIRS:
    a, b = x + dx, y + dy
    ...
</code></pre>
<h2>最终优化版</h2>
<pre><code>from collections import deque
from math import inf


class Solution:
    def findSafeWalk(self, grid: List[List[int]], health: int) -&gt; bool:
        m, n = len(grid), len(grid[0])
        d = [[inf] * n for _ in range(m)]
        d[0][0] = grid[0][0]

        q = deque([(0, 0)])
        DIRS = [(0, 1), (0, -1), (1, 0), (-1, 0)]

        while q:
            x, y = q.popleft()
            if x == m - 1 and y == n - 1:
                return d[x][y] &lt; health
            for dx, dy in DIRS:
                a, b = x + dx, y + dy
                if 0 &lt;= a &lt; m and 0 &lt;= b &lt; n:
                    nd = d[x][y] + grid[a][b]
                    if nd &lt; d[a][b]:
                        d[a][b] = nd
                        if grid[a][b]:
                            q.append((a, b))
                        else:
                            q.appendleft((a, b))
        return False
</code></pre>
<h2>小结</h2>
<table>
<thead>
<tr>
<th>写法</th>
<th>优点</th>
<th>场景</th>
</tr>
</thead>
<tbody>
<tr>
<td>原版（全遍历后判断）</td>
<td>最简洁，逻辑直白</td>
<td>日常刷题、不追求极致性能</td>
</tr>
<tr>
<td>提前返回版</td>
<td>大多数情况能提前结束</td>
<td>网格较大、终点附近安全格子多时</td>
</tr>
<tr>
<td>方向数组版</td>
<td>常数优化，扩展性好</td>
<td>竞赛、八方向变体等扩展场景</td>
</tr>
</tbody>
</table>
<p>本题 $m, n \le 50$，三种写法的实际运行时间差异在毫秒级，选择自己最熟悉的即可。</p>
]]></content>
    <author><name>Hygen</name></author>
    <category term="DSA"/>
  </entry>
  <entry>
    <title>合并区间</title>
    <link href="https://www.hygen.red/posts/hot100/05-%E6%99%AE%E9%80%9A%E6%95%B0%E7%BB%84/014%E5%90%88%E5%B9%B6%E5%8C%BA%E9%97%B4/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/hot100/05-%E6%99%AE%E9%80%9A%E6%95%B0%E7%BB%84/014%E5%90%88%E5%B9%B6%E5%8C%BA%E9%97%B4/</id>
    <published>2026-07-01T00:00:00.000Z</published>
    <updated>2026-07-01T00:00:00.000Z</updated>
    <summary>Leetcode-56</summary>
    <content type="html"><![CDATA[<h1>题目描述</h1>
<p><a href="https://leetcode.cn/problems/merge-intervals/description/?envType=study-plan-v2&amp;envId=top-100-liked">题目链接</a></p>
<p>以数组 <code>intervals</code> 表示若干个区间的集合，其中单个区间为 <code>intervals[i] = [start_i, end_i]</code>。请你合并所有重叠的区间，并返回 <em>一个不重叠的区间数组，该数组需恰好覆盖输入中的所有区间</em>。</p>
<h2>输入</h2>
<p>二维整数数组 <code>intervals</code>，每个元素是 <code>[start_i, end_i]</code> 表示一个区间</p>
<h2>输出</h2>
<p>合并重叠后的区间数组</p>
<h2>约束条件</h2>
<ul>
<li><code>1 &lt;= intervals.length &lt;= 10^4</code></li>
<li><code>intervals[i].length == 2</code></li>
<li><code>0 &lt;= start_i &lt;= end_i &lt;= 10^4</code></li>
</ul>
<h2>示例1</h2>
<pre><code>输入：intervals = [[1,3],[2,6],[8,10],[15,18]]
输出：[[1,6],[8,10],[15,18]]
</code></pre>
<p>解释：区间 <code>[1,3]</code> 和 <code>[2,6]</code> 重叠，将它们合并为 <code>[1,6]</code>。</p>
<h2>示例2</h2>
<pre><code>输入：intervals = [[1,4],[4,5]]
输出：[[1,5]]
</code></pre>
<p>解释：区间 <code>[1,4]</code> 和 <code>[4,5]</code> 在端点 <code>4</code> 处相接，视为重叠区间，合并为 <code>[1,5]</code>。</p>
<h1>解题思路</h1>
<p>这道题的核心是判断两个区间是否重叠，以及如何高效地将重叠的区间合并。两个区间 $[a, b]$ 和 $[c, d]$ 重叠的充要条件是：</p>
<p>$$a \le d \quad \text{且} \quad c \le b$$</p>
<p>换句话说——只要一个区间的起点不超过另一个区间的终点，它们就有交集。合并后的区间就是 $[\min(a,c),\ \max(b,d)]$。</p>
<blockquote>
<p>💡 <strong>一句话概括</strong>：把区间按起点排序，然后从左到右扫描——当前区间能和上一个合并就合并，不能就"封存"上一个，开始一个新的。</p>
</blockquote>
<h2>从暴力枚举出发</h2>
<p>最朴素的想法是：反复扫描所有区间对，发现重叠就合并，合并后重新扫描，直到没有任何区间可以合并为止：</p>
<pre><code>class Solution:
    def merge(self, intervals: List[List[int]]) -&gt; List[List[int]]:
        if not intervals:
            return []

        res = []
        used = [False] * len(intervals)

        for i in range(len(intervals)):
            if used[i]:
                continue
            l, r = intervals[i]
            # 反复扫描，不断扩展当前区间，直到无法再扩展
            changed = True
            while changed:
                changed = False
                for j in range(len(intervals)):
                    if used[j]:
                        continue
                    a, b = intervals[j]
                    if a &lt;= r and l &lt;= b:      # 发现重叠
                        l = min(l, a)
                        r = max(r, b)
                        used[j] = True
                        changed = True
            res.append([l, r])

        return res
</code></pre>
<p>以每个未合并区间为起点，反复扫描剩余区间并不断吞并重叠者。最坏情况下（区间呈"链式重叠"，每次只合并一个），复杂度达到 $O(n^2)$。</p>
<p><strong>瓶颈分析</strong>：暴力法花了大量时间在"寻找与当前区间重叠的下一个区间"上——区间是无序的，每次都要全量扫描。如果能<strong>让区间按某种顺序排列</strong>，使得可能重叠的区间彼此相邻，一次从左到右的扫描就能完成所有合并。</p>
<blockquote>
<p>🧠 <strong>直觉理解</strong>：想象你面前有一堆纸条，每张纸条写着起止时间，散乱地摊在桌上。暴力法是你随手拿起一张，然后翻遍全桌找能和它接上的——找到一张接一张，接不上了再换下一张。如果先把纸条按起始时间从左到右排好，你会发现——<strong>能合并的纸条一定彼此相邻</strong>，从左到右一趟就能全部处理完。</p>
</blockquote>
<h2>排序 + 贪心扫描</h2>
<p>将所有区间按<strong>起点</strong>升序排序后，一个关键性质浮出水面：</p>
<blockquote>
<p><strong>性质</strong>：排序后，如果当前区间 $[x, y]$ 不能与前面正在合并的区间 $[l, r]$ 合并（即 $x &gt; r$），那么后面所有的区间也不可能与 $[l, r]$ 合并——因为后面的起点只会更大。</p>
</blockquote>
<p>这意味着我们可以用"甩手掌柜"的方式处理：维护一个"当前正在合并"的区间 $[l, r]$，从左到右扫描每个区间——</p>
<ul>
<li>如果 $x \le r$（重叠），就扩展右边界：$r = \max(r, y)$</li>
<li>如果 $x &gt; r$（不重叠），就把 $[l, r]$ 存入结果，然后开启一个新的合并区间 $[l, r] = [x, y]$</li>
</ul>
<p>扫描结束后，别忘了把最后一个 $[l, r]$ 也存进去。</p>
<pre><code>class Solution:
    def merge(self, intervals: List[List[int]]) -&gt; List[List[int]]:
        intervals.sort()           # 按起点排序
        res = []
        l, r = intervals[0]        # 当前正在合并的区间

        for x, y in intervals:
            if x &lt;= r:              # 重叠：扩展右边界
                r = max(r, y)
            else:                   # 不重叠：封存当前区间，开启新区间
                res.append([l, r])
                l, r = x, y

        res.append([l, r])          # 封存最后一个区间
        return res
</code></pre>
<blockquote>
<p>⚠️ <strong>注意</strong>：<code>intervals.sort()</code> 对二元列表默认按第一个元素排序，第一个相同时按第二个元素排序。这里我们只需要按起点排序，<code>intervals.sort()</code> 刚好满足。如果面试官要求显式表达，可以写成 <code>intervals.sort(key=lambda x: x[0])</code>。</p>
</blockquote>
<h3>执行过程追踪</h3>
<p>以 <code>intervals = [[1,3],[2,6],[8,10],[15,18]]</code> 为例（排序后不变）：</p>
<table>
<thead>
<tr>
<th>步骤</th>
<th>当前 <code>[x,y]</code></th>
<th><code>l</code></th>
<th><code>r</code></th>
<th>判断</th>
<th>操作</th>
<th><code>res</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>初始</td>
<td>—</td>
<td>1</td>
<td>3</td>
<td>—</td>
<td>—</td>
<td><code>[]</code></td>
</tr>
<tr>
<td>1</td>
<td><code>[1,3]</code></td>
<td>1</td>
<td>3</td>
<td><code>1 ≤ 3</code></td>
<td><code>r = max(3, 3) = 3</code>（无变化）</td>
<td><code>[]</code></td>
</tr>
<tr>
<td>2</td>
<td><code>[2,6]</code></td>
<td>1</td>
<td>3</td>
<td><code>2 ≤ 3</code></td>
<td><code>r = max(3, 6)</code> = <strong>6</strong></td>
<td><code>[]</code></td>
</tr>
<tr>
<td>3</td>
<td><code>[8,10]</code></td>
<td>1</td>
<td>6</td>
<td><code>8 &gt; 6</code></td>
<td>保存 <code>[1, 6]</code>，新 <code>l=8, r=10</code></td>
<td><code>[[1,6]]</code></td>
</tr>
<tr>
<td>4</td>
<td><code>[15,18]</code></td>
<td>8</td>
<td>10</td>
<td><code>15 &gt; 10</code></td>
<td>保存 <code>[8,10]</code>，新 <code>l=15, r=18</code></td>
<td><code>[[1,6],[8,10]]</code></td>
</tr>
<tr>
<td>结束</td>
<td>—</td>
<td>15</td>
<td>18</td>
<td>—</td>
<td>保存 <code>[15,18]</code></td>
<td><code>[[1,6],[8,10],[15,18]]</code></td>
</tr>
</tbody>
</table>
<blockquote>
<p>💡 <strong>小贴士</strong>：注意步骤 1——<code>l, r</code> 由 <code>intervals[0]</code> 初始化，然后 <code>for</code> 循环再次遍历到了 <code>intervals[0]</code>。此时 <code>x = l</code> 且 <code>y = r</code>，所以 <code>x ≤ r</code> 恒成立，<code>r = max(r, y)</code> 不改变任何值，属于一次"空操作"。这一步看似多余，但它让循环逻辑保持统一——<strong>所有区间（包括第一个）都经过同一套判断逻辑</strong>，代码更简洁，且 $O(1)$ 的额外开销完全可以忽略。</p>
</blockquote>
<p>再来看示例 2 <code>[[1,4],[4,5]]</code>，端点相接的情况：</p>
<table>
<thead>
<tr>
<th>步骤</th>
<th>当前 <code>[x,y]</code></th>
<th><code>l</code></th>
<th><code>r</code></th>
<th>判断</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>初始</td>
<td>—</td>
<td>1</td>
<td>4</td>
<td>—</td>
<td>—</td>
</tr>
<tr>
<td>1</td>
<td><code>[1,4]</code></td>
<td>1</td>
<td>4</td>
<td><code>1 ≤ 4</code></td>
<td><code>r = max(4, 4) = 4</code></td>
</tr>
<tr>
<td>2</td>
<td><code>[4,5]</code></td>
<td>1</td>
<td>4</td>
<td><code>4 ≤ 4</code></td>
<td><code>r = max(4, 5)</code> = <strong>5</strong></td>
</tr>
<tr>
<td>结束</td>
<td>—</td>
<td>1</td>
<td>5</td>
<td>—</td>
<td>保存 <code>[1,5]</code></td>
</tr>
</tbody>
</table>
<p>关键在步骤 2：判断条件 <code>x ≤ r</code> 即 <code>4 ≤ 4</code> 成立，因此 <code>[4,5]</code> 被视为与 <code>[1,4]</code> 重叠。这正是题目要求的——端点相接也算重叠。</p>
<h3>为什么贪心是对的？</h3>
<p>排序后从左到右扫描，每次遇到不重叠的区间就"封存"当前——为什么这样不会错过更优的合并方式？</p>
<blockquote>
<p>🧠 <strong>直觉理解</strong>：排序保证了区间的起点是单调递增的。假设当前合并区间 $[l, r]$ 与某个后面的区间 $[x, y]$ 不重叠（即 $x &gt; r$），那 $[x, y]$ 之后的所有区间起点 $\ge x &gt; r$，更不可能与 $[l, r]$ 重叠。所以此时"封存"$[l, r]$ 一定是安全的——它已经"长到最大了"，不会再被任何后续区间扩展。</p>
</blockquote>
<h2>复杂度</h2>
<ul>
<li><strong>时间复杂度</strong>：$O(n \log n)$，其中 $n$ 是区间个数。排序占 $O(n \log n)$，扫描占 $O(n)$。</li>
<li><strong>空间复杂度</strong>：$O(1)$（不计输出数组 <code>res</code>），或 $O(n)$（计入输出）。仅使用了 <code>l</code>、<code>r</code> 两个额外变量。</li>
</ul>
<p>$O(n \log n)$ 已是理论最优——因为区间合并问题至少需要排序，而比较排序的下界就是 $O(n \log n)$。如果输入本身已按起点有序（如某些变体题目的设定），则可做到 $O(n)$。</p>
<blockquote>
<p>⚠️ <strong>注意</strong>：Python 的 <code>list.sort()</code> 是原地排序，空间复杂度为 $O(1)$（TimSort 的临时空间为 $O(n)$ 但通常不计入）。如果你不希望修改输入数组，可以 <code>sorted(intervals)</code> 代替，但空间变为 $O(n)$。</p>
</blockquote>
<h1>代码优化</h1>
<h2>写法对比：<code>l, r</code> vs <code>res[-1]</code></h2>
<p>上面使用了 <code>l, r</code> 两个变量追踪正在合并的区间。另一种常见写法是直接在结果数组的最后一个区间上操作：</p>
<pre><code>class Solution:
    def merge(self, intervals: List[List[int]]) -&gt; List[List[int]]:
        intervals.sort()
        res = [intervals[0]]

        for x, y in intervals[1:]:
            if x &lt;= res[-1][1]:
                res[-1][1] = max(res[-1][1], y)
            else:
                res.append([x, y])

        return res
</code></pre>
<p>两种写法的对比：</p>
<table>
<thead>
<tr>
<th>对比项</th>
<th><code>l, r</code> 变量</th>
<th><code>res[-1]</code> 操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>可读性</td>
<td>⭐⭐⭐⭐ 变量名直观</td>
<td>⭐⭐⭐ <code>[-1]</code> 需要一点适应</td>
</tr>
<tr>
<td>简洁度</td>
<td>⭐⭐⭐ 需要尾部 <code>append</code></td>
<td>⭐⭐⭐⭐⭐ 循环结束即完成</td>
</tr>
<tr>
<td>避免浅拷贝问题</td>
<td>✅ 直接用 <code>l, r</code></td>
<td>⚠️ 修改 <code>res[-1][1]</code> 是原地修改，恰好安全</td>
</tr>
<tr>
<td>适用场景</td>
<td>适合讲解、教学</td>
<td>适合生产、竞赛</td>
</tr>
</tbody>
</table>
<p><code>l, r</code> 版本的好处是思路更清晰——"我正在合并一个区间，还没想好要不要放进结果"。<code>res[-1]</code> 版本的好处是代码更紧凑——边界处理被 <code>res</code> 初始化吃掉，循环从 <code>intervals[1:]</code> 开始，省去了尾部的那句 <code>res.append([l, r])</code>。</p>
<blockquote>
<p>💡 <strong>小贴士</strong>：在面试中建议先写出 <code>l, r</code> 版本，把思路讲清楚；如果面试官要求更简洁，再优化到 <code>res[-1]</code> 版本。一上来就甩出 <code>res[-1]</code> 可能会让面试官觉得你在背模板。</p>
</blockquote>
<h2><code>intervals.sort()</code> vs <code>intervals.sort(key=lambda x: x[0])</code></h2>
<p>两种写法在本题中完全等价——因为 Python 的列表比较是按元素顺序逐个比较的：</p>
<ul>
<li><code>[1, 3] &lt; [2, 6]</code> 先比第一个元素 <code>1 &lt; 2</code>，结果为 <code>True</code>，不再比第二个</li>
<li><code>[1, 3] &lt; [1, 4]</code> 第一个相等 <code>1 == 1</code>，比第二个 <code>3 &lt; 4</code></li>
</ul>
<p>这意味着 <code>intervals.sort()</code> 等价于 "按起点排序，起点相同时按终点排序"。而题目只需要按起点排序——起点相同时按终点排不排都不影响正确性，但恰好也能正常工作。</p>
<p>显式写成 <code>key=lambda x: x[0]</code> 的好处是意图表达更明确，但性能上反而略慢（多了 lambda 调用开销）。两种写法都可以，看个人偏好。</p>
<h2>处理空输入</h2>
<p>虽然约束条件保证 <code>n &gt;= 1</code>，但在工程实践中防御式编程不会错：</p>
<pre><code>class Solution:
    def merge(self, intervals: List[List[int]]) -&gt; List[List[int]]:
        if not intervals:
            return []
        # ...
</code></pre>
<p>如果约束真的可能为 0，<code>intervals[0]</code> 会在初始化 <code>l, r</code> 时抛出 <code>IndexError</code>。</p>
<h1>总结</h1>
<p>「合并区间」是贪心算法的经典入门题。它的核心技巧只有三步：</p>
<ol>
<li><strong>排序</strong>——让区间按起点有序</li>
<li><strong>扫描</strong>——从左到右逐个判断是否重叠</li>
<li><strong>合并</strong>——重叠就扩展，不重叠就封存</li>
</ol>
<p>这个"排序 + 一次扫描"的模式广泛适用于一大类区间问题。掌握了它，下面这些题目你会觉得似曾相识：</p>
<ul>
<li><strong><a href="https://leetcode.cn/problems/insert-interval/">57. 插入区间</a></strong>：给定一个已排序的不重叠区间列表，插入新区间后合并——相当于"手动的排序 + 合并"中的合并步骤</li>
<li><strong><a href="https://leetcode.cn/problems/non-overlapping-intervals/">435. 无重叠区间</a></strong>：移除最少的区间使剩余区间不重叠——同样是排序 + 贪心，但贪心策略不同</li>
<li><strong><a href="https://leetcode.cn/problems/minimum-number-of-arrows-to-burst-balloons/">452. 用最少数量的箭引爆气球</a></strong>：气球 = 区间，箭 = 公共交点——排序 + 贪心扫描的变体</li>
</ul>
<blockquote>
<p>🧠 <strong>核心收获</strong>：区间问题八成从排序入手，排序后重叠的区间会"挤在一起"，剩下的就是设计贪心扫描逻辑。别被题面吓到，先问自己——"如果区间已经排好序了，怎么做？"</p>
</blockquote>
]]></content>
    <author><name>Hygen</name></author>
    <category term="hot100"/>
  </entry>
  <entry>
    <title>轮转数组</title>
    <link href="https://www.hygen.red/posts/hot100/05-%E6%99%AE%E9%80%9A%E6%95%B0%E7%BB%84/015%E8%BD%AE%E8%BD%AC%E6%95%B0%E7%BB%84/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/hot100/05-%E6%99%AE%E9%80%9A%E6%95%B0%E7%BB%84/015%E8%BD%AE%E8%BD%AC%E6%95%B0%E7%BB%84/</id>
    <published>2026-07-01T00:00:00.000Z</published>
    <updated>2026-07-01T00:00:00.000Z</updated>
    <summary>Leetcode-189</summary>
    <content type="html"><![CDATA[<h1>题目描述</h1>
<p><a href="https://leetcode.cn/problems/rotate-array/description/?envType=study-plan-v2&amp;envId=top-100-liked">题目链接</a></p>
<p>给定一个整数数组 <code>nums</code>，将数组中的元素向右轮转 <code>k</code> 个位置，其中 <code>k</code> 是非负数。</p>
<h2>输入</h2>
<p>整数数组 <code>nums</code> 和非负整数 <code>k</code></p>
<h2>输出</h2>
<p>原地修改 <code>nums</code>，将其向右轮转 <code>k</code> 步</p>
<h2>约束条件</h2>
<ul>
<li><code>1 &lt;= nums.length &lt;= 10^5</code></li>
<li><code>-2^31 &lt;= nums[i] &lt;= 2^31 - 1</code></li>
<li><code>0 &lt;= k &lt;= 10^5</code></li>
</ul>
<h2>进阶约束</h2>
<ul>
<li>尽可能想出更多的解决方案，至少有 <strong>三种</strong> 不同的方法可以解决这个问题。</li>
<li>你可以使用空间复杂度为 $O(1)$ 的 <strong>原地</strong> 算法解决这个问题吗？</li>
</ul>
<h2>示例1</h2>
<pre><code>输入：nums = [1,2,3,4,5,6,7], k = 3
输出：[5,6,7,1,2,3,4]
</code></pre>
<p>解释：</p>
<pre><code>向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]
</code></pre>
<h2>示例2</h2>
<pre><code>输入：nums = [-1,-100,3,99], k = 2
输出：[3,99,-1,-100]
</code></pre>
<p>解释：</p>
<pre><code>向右轮转 1 步: [99,-1,-100,3]
向右轮转 2 步: [3,99,-1,-100]
</code></pre>
<h1>解题思路</h1>
<p>向右轮转 <code>k</code> 步，本质上是把数组末尾的 <code>k</code> 个元素"搬"到数组开头。最直接的想法是——创建一个新数组，把每个元素放到它该去的位置。</p>
<blockquote>
<p>💡 <strong>一句话概括</strong>：轮转数组有三种经典解法——额外数组（最直观）、环状替换（最巧妙）、三次反转（最优雅）。其中三次反转法代码最短、最容易写对，是面试的首选。</p>
</blockquote>
<h2>从额外数组出发</h2>
<p>最简单的思路：创建一个新数组 <code>ans</code>，将 <code>nums[i]</code> 放到 <code>ans[(i + k) % n]</code> 的位置，最后把新数组拷贝回 <code>nums</code>：</p>
<pre><code>class Solution:
    def rotate(self, nums: List[int], k: int) -&gt; None:
        n = len(nums)
        k %= n                          # 处理 k &gt;= n 的情况
        ans = [0] * n

        for i in range(n):
            ans[(i + k) % n] = nums[i]

        nums[:] = ans                   # 拷贝回原数组
</code></pre>
<blockquote>
<p>⚠️ <strong>注意</strong>：<code>k %= n</code> 这一行很关键。当 <code>k &gt;= n</code> 时（如 <code>k = 10, n = 7</code>），轮转 <code>k</code> 步等价于轮转 <code>k % n = 3</code> 步。虽然不取模的话 <code>(i + k) % n</code> 也能算出正确位置，但轮转超过一整圈纯属浪费。</p>
</blockquote>
<p>这里用 <code>nums[:] = ans</code> 而不是 <code>nums = ans</code>——前者修改列表的<strong>内容</strong>（原地替换），后者只是让局部变量 <code>nums</code> 指向新列表（不会影响函数外部的原数组）。</p>
<p><strong>瓶颈分析</strong>：这个方法需要 $O(n)$ 的额外空间。$n \le 10^5$ 时，$10^5$ 个整数的数组大约占 800KB——可以通过，但进阶要求挑战 $O(1)$ 空间。</p>
<blockquote>
<p>🧠 <strong>直觉理解</strong>：创建新数组就像搬家时先把所有家具搬到临时仓库，再按新布局搬回去——稳，但需要两倍的空间。能不能直接在原地"挪"家具？</p>
</blockquote>
<h2>三次反转法</h2>
<p>这是本题最优雅的解法。将 <code>nums</code> 向右轮转 <code>k</code> 步，等价于以下三步：</p>
<ol>
<li><strong>反转整个数组</strong>——让末尾的 <code>k</code> 个元素跑到开头（此时是逆序的），开头的 <code>n-k</code> 个元素跑到末尾（同样逆序）</li>
<li><strong>反转前 <code>k</code> 个元素</strong>——恢复它们原本的顺序</li>
<li><strong>反转后 <code>n-k</code> 个元素</strong>——恢复它们原本的顺序</li>
</ol>
<p>用数学语言描述：设原数组为 $A$，目标是将 $A$ 变为 $A[n-k:] + A[:n-k]$。注意到：</p>
<p>$$\text{reverse}(A)[:k] = \text{reverse}(A[n-k:])$$
$$\text{reverse}(A)[k:] = \text{reverse}(A[:n-k])$$</p>
<p>因此 $\text{reverse}(\text{reverse}(A)[:k]) + \text{reverse}(\text{reverse}(A)[k:]) = A[n-k:] + A[:n-k]$，恰好是目标结果。</p>
<h3>执行过程可视化</h3>
<p>以 <code>nums = [1, 2, 3, 4, 5, 6, 7], k = 3</code> 为例：</p>
<table>
<thead>
<tr>
<th>步骤</th>
<th>操作</th>
<th>数组状态</th>
</tr>
</thead>
<tbody>
<tr>
<td>初始</td>
<td>—</td>
<td><code>[1, 2, 3, 4, 5, 6, 7]</code></td>
</tr>
<tr>
<td>1</td>
<td><code>nums.reverse()</code> —— 反转整个数组</td>
<td><code>[7, 6, 5, 4, 3, 2, 1]</code></td>
</tr>
<tr>
<td>2</td>
<td>反转前 <code>k=3</code> 个元素 <code>[7, 6, 5]</code></td>
<td><code>[5, 6, 7, 4, 3, 2, 1]</code></td>
</tr>
<tr>
<td>3</td>
<td>反转后 <code>n-k=4</code> 个元素 <code>[4, 3, 2, 1]</code></td>
<td><code>[5, 6, 7, 1, 2, 3, 4]</code> ✅</td>
</tr>
</tbody>
</table>
<p>步骤 1 之后发生了什么？原本在末尾的 <code>[5, 6, 7]</code> 被翻到了开头（变成 <code>[7, 6, 5]</code>），原本在开头的 <code>[1, 2, 3, 4]</code> 被翻到了末尾（变成 <code>[4, 3, 2, 1]</code>）。此时两段的位置已经正确了，只是各自内部是逆序的——于是步骤 2 和 3 各翻转一次，恢复原序。</p>
<blockquote>
<p>🧠 <strong>直觉理解</strong>：想象你手里有一叠牌，要把最上面的 3 张挪到最下面。三次反转法就是——① 整叠牌翻个面 ② 前 3 张翻回来 ③ 剩下的翻回来。每张牌经过两次翻转，方向最终还是正的，但整体顺序已经发生了变化。</p>
</blockquote>
<p>再以 <code>nums = [-1, -100, 3, 99], k = 2</code> 为例：</p>
<table>
<thead>
<tr>
<th>步骤</th>
<th>操作</th>
<th>数组状态</th>
</tr>
</thead>
<tbody>
<tr>
<td>初始</td>
<td>—</td>
<td><code>[-1, -100, 3, 99]</code></td>
</tr>
<tr>
<td>1</td>
<td>反转整个数组</td>
<td><code>[99, 3, -100, -1]</code></td>
</tr>
<tr>
<td>2</td>
<td>反转前 <code>k=2</code> 个元素 <code>[99, 3]</code></td>
<td><code>[3, 99, -100, -1]</code></td>
</tr>
<tr>
<td>3</td>
<td>反转后 <code>n-k=2</code> 个元素 <code>[-100, -1]</code></td>
<td><code>[3, 99, -1, -100]</code> ✅</td>
</tr>
</tbody>
</table>
<h3>完整代码</h3>
<pre><code>class Solution:
    def rotate(self, nums: List[int], k: int) -&gt; None:
        """
        Do not return anything, modify nums in-place instead.
        """
        k %= len(nums)
        nums.reverse()
        nums[:] = nums[:k][::-1] + nums[k:][::-1]
</code></pre>
<p>逐行解释：</p>
<ol>
<li>
<p><strong><code>k %= len(nums)</code></strong>：处理 <code>k &gt;= n</code> 的情况。当 <code>k = n</code> 时，轮转后数组不变；当 <code>k &gt; n</code> 时，每 <code>n</code> 步回到原位，只需轮转 <code>k % n</code> 步。</p>
</li>
<li>
<p><strong><code>nums.reverse()</code></strong>：反转整个数组。这是 Python 列表的原地方法，将数组首尾颠倒。</p>
</li>
<li>
<p><strong><code>nums[:k][::-1]</code></strong>：取前 <code>k</code> 个元素（即原数组末尾 <code>k</code> 个元素的逆序），再 <code>[::-1]</code> 反转一次——恢复成原本的顺序。</p>
</li>
<li>
<p><strong><code>nums[k:][::-1]</code></strong>：取后 <code>n-k</code> 个元素（即原数组开头 <code>n-k</code> 个元素的逆序），同样反转恢复原序。</p>
</li>
<li>
<p><strong><code>nums[:] = ...</code></strong>：用全切片赋值，确保<strong>原地修改</strong>列表内容，而非创建新列表。</p>
</li>
</ol>
<h3>为什么三次反转是对的？</h3>
<p>从索引的角度推导：设 $n = \text{len(nums)}$。原数组中位置 <code>i</code> 的元素，经过第一次整体反转后到了位置 $n - 1 - i$。然后：</p>
<ul>
<li>若 $n - 1 - i &lt; k$（即元素原本在末尾 <code>k</code> 个中），它会在步骤 2 中被再次翻转，最终位置是 $k - 1 - (n - 1 - i) = i + k - n$</li>
<li>若 $n - 1 - i \ge k$（即元素原本在开头 <code>n-k</code> 个中），它会在步骤 3 中被再次翻转，最终位置是 $(n - 1) - (n - 1 - i - k) = i + k$</li>
</ul>
<p>两种情况恰好对应了轮转 <code>k</code> 步后每个元素应有的位置。</p>
<blockquote>
<p>💡 <strong>小贴士</strong>：索引推导虽然严谨，但不如画图直观。建议用纸笔跑一遍示例，比公式更容易理解。</p>
</blockquote>
<h2>复杂度</h2>
<ul>
<li><strong>时间复杂度</strong>：$O(n)$。反转整个数组是 $O(n)$，两次切片反转分别是 $O(k)$ 和 $O(n-k)$，总复杂度 $O(n)$。</li>
<li><strong>空间复杂度</strong>：$O(1)$（算法层面）。<code>nums.reverse()</code> 是原地操作。<code>nums[:k][::-1]</code> 会创建 $O(k)$ 的临时列表，<code>nums[k:][::-1]</code> 会创建 $O(n-k)$ 的临时列表，但这些临时对象在赋值完成后立即被 GC 回收。若面试官要求严格的 $O(1)$，见下方"代码优化"章节的手动反转版本。</li>
</ul>
<h1>解题思路（环状替换）</h1>
<p>除三次反转外，另一种 $O(1)$ 空间的原地算法是<strong>环状替换</strong>。思路是：从位置 0 开始，将 <code>nums[0]</code> 移到 <code>nums[(0 + k) % n]</code>，再将被挤走的元素移到它的目标位置，如此往复，直到回到起点。如果一轮环结束后还有元素未处理，从下一个位置开启新环。</p>
<pre><code>class Solution:
    def rotate(self, nums: List[int], k: int) -&gt; None:
        n = len(nums)
        k %= n
        count = 0                     # 已移动的元素个数

        for start in range(n):
            if count &gt;= n:            # 所有元素都已就位
                break

            cur = start
            prev = nums[start]

            while True:
                nxt = (cur + k) % n
                prev, nums[nxt] = nums[nxt], prev
                cur = nxt
                count += 1

                if cur == start:      # 回到起点，本轮环结束
                    break
</code></pre>
<p>外层循环 <code>for start</code> 恰好执行 $\gcd(n, k)$ 次——每个独立的环包含 $n / \gcd(n, k)$ 个元素。</p>
<blockquote>
<p>💡 <strong>小贴士</strong>：环状替换的思路非常精妙，体现了置换群中"轮换分解"的思想。但实现时容易出错的点很多——何时停止、<code>prev</code> 的更新顺序、<code>start</code> 的推进条件……相比之下，三次反转法几乎不会写错。</p>
</blockquote>
<p>三种方法的对比：</p>
<table>
<thead>
<tr>
<th>对比项</th>
<th>额外数组</th>
<th>环状替换</th>
<th>三次反转</th>
</tr>
</thead>
<tbody>
<tr>
<td>时间复杂度</td>
<td>$O(n)$</td>
<td>$O(n)$</td>
<td>$O(n)$</td>
</tr>
<tr>
<td>空间复杂度</td>
<td>$O(n)$</td>
<td>$O(1)$</td>
<td>$O(1)$</td>
</tr>
<tr>
<td>代码简洁度</td>
<td>⭐⭐⭐⭐</td>
<td>⭐⭐</td>
<td>⭐⭐⭐⭐⭐</td>
</tr>
<tr>
<td>思维难度</td>
<td>⭐</td>
<td>⭐⭐⭐⭐⭐</td>
<td>⭐⭐</td>
</tr>
<tr>
<td>容易写对</td>
<td>✅</td>
<td>⚠️ 容易出错</td>
<td>✅</td>
</tr>
</tbody>
</table>
<blockquote>
<p>🧠 <strong>面试建议</strong>：三种方法都值得了解，但如果只记一种——记三次反转。它代码最短、最不容易出错，且能自然扩展到"向左轮转"（先反转前 <code>k</code> 个和后 <code>n-k</code> 个，再整体反转）。</p>
</blockquote>
<h1>代码优化</h1>
<h2>手动实现子区间反转（严格的 $O(1)$ 空间）</h2>
<p>用户代码中 <code>nums[:k][::-1]</code> 虽然简洁，但切片 <code>[::-1]</code> 会创建临时列表。如果面试官追问空间复杂度，可以手动实现区间反转：</p>
<pre><code>class Solution:
    def rotate(self, nums: List[int], k: int) -&gt; None:
        n = len(nums)
        k %= n

        def reverse(l: int, r: int) -&gt; None:
            """反转 nums[l:r]（左闭右开）"""
            r -= 1
            while l &lt; r:
                nums[l], nums[r] = nums[r], nums[l]
                l += 1
                r -= 1

        reverse(0, n)       # 反转整个数组
        reverse(0, k)       # 反转前 k 个
        reverse(k, n)       # 反转后 n-k 个
</code></pre>
<p>这个版本没有任何额外数组分配，空间复杂度严格为 $O(1)$。<code>reverse</code> 函数使用双指针交换，每对元素交换一次，总共 $n$ 次交换。</p>
<h2>切片反转 vs 手动反转</h2>
<table>
<thead>
<tr>
<th>对比项</th>
<th><code>nums[:k][::-1]</code></th>
<th><code>reverse(0, k)</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>空间复杂度</td>
<td>$O(k)$（创建临时列表）</td>
<td>$O(1)$</td>
</tr>
<tr>
<td>代码行数</td>
<td>1 行</td>
<td>需要辅助函数</td>
</tr>
<tr>
<td>Pythonic</td>
<td>✅ 非常 Pythonic</td>
<td>⭐ 更偏底层</td>
</tr>
<tr>
<td>适合场景</td>
<td>日常刷题、快速通过</td>
<td>面试官要求严格 $O(1)$</td>
</tr>
</tbody>
</table>
<p>两种写法都是正确的，选择取决于场景。日常刷题用第一种（够简洁），面试建议先写第一种，然后主动提一句"如果需要严格 $O(1)$，可以手动实现子区间反转"——这会让面试官觉得你对细节有掌控力。</p>
<h2>向左轮转</h2>
<p>如果题目改为向左轮转 <code>k</code> 步，只需调整反转顺序：</p>
<pre><code># 向右轮转 k 步：整体反转 → 反转前 k → 反转后 n-k
reverse(0, n)
reverse(0, k)
reverse(k, n)

# 向左轮转 k 步：反转前 k → 反转后 n-k → 整体反转
reverse(0, k)
reverse(k, n)
reverse(0, n)
</code></pre>
<blockquote>
<p>💡 <strong>小贴士</strong>：想不通为什么？"向右轮转 $k$ 步"等价于"向左轮转 $n-k$ 步"，把 $k$ 换成 $n-k$ 代入向右轮转的公式就清楚了。</p>
</blockquote>
<h1>总结</h1>
<p>「轮转数组」是数组操作的经典题目，它的核心在于理解"轮转"的等价变换——将末尾元素移到开头。</p>
<p>三种解法各有千秋：</p>
<ul>
<li><strong>额外数组</strong>：最直观，适合快速确认思路</li>
<li><strong>环状替换</strong>：最巧妙，体现了置换群中轮换分解的思想</li>
<li><strong>三次反转</strong>：最优雅，代码短且不易出错，是面试首选</li>
</ul>
<blockquote>
<p>🧠 <strong>核心收获</strong>：数组轮转 = 三次反转。这个技巧不仅适用于本题，在「反转字符串中的单词」（LeetCode 151）、「左旋转字符串」（剑指 Offer 58-II）等题目中也有类似的思路——先整体反转，再局部修正。</p>
</blockquote>
<p>解决本题后，可以挑战以下延伸题目：</p>
<ul>
<li><strong><a href="https://leetcode.cn/problems/rotate-list/">61. 旋转链表</a></strong>：将数组换成链表——同样是轮转，但链表无法直接反转，需要找断点重连</li>
<li><strong><a href="https://leetcode.cn/problems/find-minimum-in-rotated-sorted-array/">153. 寻找旋转排序数组中的最小值</a></strong>：一个已排序数组被轮转后，如何用二分查找找到最小值？</li>
<li><strong><a href="https://leetcode.cn/problems/rotate-string/">796. 旋转字符串</a></strong>：判断一个字符串是否是另一个字符串轮转得到的——巧妙的拼接技巧</li>
</ul>
]]></content>
    <author><name>Hygen</name></author>
    <category term="hot100"/>
  </entry>
  <entry>
    <title>Kadane 算法详解</title>
    <link href="https://www.hygen.red/posts/dsa/knowledge/kadane/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/dsa/knowledge/kadane/</id>
    <published>2026-06-30T00:00:00.000Z</published>
    <updated>2026-06-30T00:00:00.000Z</updated>
    <summary>深入理解 Kadane 最大子数组和算法——从 DP 推导到贪心直觉</summary>
    <content type="html"><![CDATA[<h1>什么是 Kadane 算法？</h1>
<p>Kadane 算法是解决<strong>最大子数组和</strong>问题的经典算法，由 Jay Kadane 于 1984 年提出。它能在 $O(n)$ 时间、$O(1)$ 空间内找到数组中的最大连续子数组和。</p>
<p>它的核心代码极为简短：</p>
<pre><code>def max_subarray(nums):
    ans = float('-inf')
    pre = 0
    for x in nums:
        pre = max(pre, 0) + x
        ans = max(ans, pre)
    return ans
</code></pre>
<p>寥寥几行，却浓缩了动态规划和贪心两种思想的精髓。接下来我们逐步拆解它背后的原理。</p>
<h1>核心直觉</h1>
<p>想象你是一个股民，每天都有盈亏。你需要在某一天买入、之后某一天卖出，获得最大收益——这就是著名的<strong>最大利润问题</strong>，本质上就是最大子数组和。</p>
<p>Kadane 算法的核心直觉可以浓缩为一句话：</p>
<blockquote>
<p>💡 <strong>核心直觉</strong>：在遍历数组时，我们始终面临一个选择——是<strong>延续</strong>之前的累加（pre + x），还是<strong>重新开始</strong>（x）。做这个选择的依据很简单：如果之前的累加是<strong>正数</strong>，它对我们有帮助，保留它；如果之前的累加是<strong>负数或零</strong>，它只会拖后腿，丢弃它。</p>
</blockquote>
<blockquote>
<p>🧠 <strong>换个角度</strong>：一个负数的前缀，就像背着一个越来越重的包袱爬山——越早扔掉它，越早轻装上阵。Kadane 算法的 <code>max(pre, 0)</code> 就是这个"扔包袱"的动作。</p>
</blockquote>
<h1>从动态规划的角度理解</h1>
<h2>状态定义</h2>
<p>这是理解 Kadane 算法最关键的一步。定义：</p>
<p>$$dp[i] = \text{以 } nums[i] \text{ 结尾的最大子数组和}$$</p>
<p>为什么强调"以 $i$ 结尾"？因为这个限制让子问题之间可以<strong>递推</strong>——如果我知道以 $i-1$ 结尾的最优解，我就能推导出以 $i$ 结尾的最优解。</p>
<blockquote>
<p>💡 <strong>设计 DP 的通用技巧</strong>：当问题问"任意区间的最优值"时，一个常见的套路是<strong>强制让状态以当前位置结尾</strong>，然后在所有 $dp[i]$ 中取最大值。这让我们获得了"前一个状态 + 当前元素"的递推关系。类似的设计在「最大乘积子数组」「最长上升子序列」等题目中反复出现。</p>
</blockquote>
<h2>状态转移</h2>
<p>对于位置 $i$，我们只有两种选择：</p>
<p><strong>选择一：自成一派。</strong>
从 $nums[i]$ 开始一个全新的子数组。此时子数组和为 $nums[i]$。</p>
<p><strong>选择二：延续前朝。</strong>
把 $nums[i]$ 接到以 $nums[i-1]$ 结尾的最大子数组后面。此时和为 $dp[i-1] + nums[i]$。</p>
<p>取两者中的较大值：</p>
<p>$$dp[i] = \max(nums[i],\ dp[i-1] + nums[i])$$</p>
<p>最终答案就是 $\max\limits_{0 \le i &lt; n} dp[i]$。</p>
<blockquote>
<p>🧠 <strong>直觉验证</strong>：假设你在位置 3，前面的最大子数组和是 5，当前元素是 -2。延续得到 3，自成一派得到 -2。显然 3 &gt; -2，所以延续。如果当前元素是 -10 呢？延续得到 -5，自成一派得到 -10。还是延续。只有当前面的和<strong>本身就是负数</strong>时，自成一派才更优——因为负前缀只会让当前元素更小。</p>
</blockquote>
<h2>空间优化</h2>
<p>观察转移方程：$dp[i]$ 仅依赖于 $dp[i-1]$，我们不需要维护整个 <code>dp</code> 数组。用一个变量 <code>pre</code> 代替 $dp[i-1]$：</p>
<pre><code>pre = max(x, pre + x)
</code></pre>
<p>这已经是 $O(1)$ 空间了。但我们还能再进一步——</p>
<p>注意到：当 $pre \le 0$ 时，$\max(x, pre + x) = x$（因为一个非正数加上 $x$ 不可能大于 $x$）。所以我们可以将条件前置：</p>
<p>$$pre = \max(pre, 0) + x$$</p>
<p>这就是 Kadane 算法的最终形态。语义变成了「如果之前和是正的，加上当前值；如果是负的，丢弃，从当前值重新开始」。</p>
<pre><code>pre = max(pre, 0) + x
</code></pre>
<p>两种写法完全等价，但后者更直观地体现了"丢弃负前缀"的贪心决策。</p>
<h1>从贪心角度理解</h1>
<p>换个视角，Kadane 算法也可以看作一种<strong>在线贪心</strong>：</p>
<blockquote>
<p>🧠 <strong>贪心视角</strong>：我们一边扫描数组，一边维护"当前正在考虑的子数组"。每遇到一个新元素，我们就问自己——这个元素加入后，子数组和是变大了还是变小了？如果变小了（前缀和为负），说明前面的元素全是累赘，"咔嚓"一刀切断，从当前元素重新开始。</p>
</blockquote>
<p>这里的贪心选择是<strong>局部最优的</strong>——在每一步，我们都选择"使当前子数组和最大"的决策。但这之所以能找到全局最优，是因为<strong>任何全局最大子数组不可能包含一个和为负的前缀</strong>（否则去掉这个前缀会得到一个更大的子数组）。</p>
<blockquote>
<p>💡 <strong>贪心安全性的证明</strong>：假设全局最大子数组是 $nums[L..R]$，其中 $L$ 之前有一个和为负的前缀 $nums[k..L-1]$。那么 $nums[L..R]$ 的和一定大于 $nums[k..R]$ 的和（因为负前缀拉低了总和）。因此 Kadane 算法在 $L$ 位置"丢弃"前一个负前缀的决策，恰好保留了真正的最优解。</p>
</blockquote>
<h1>为什么 Kadane 算法同时属于 DP 和贪心？</h1>
<p>这不是巧合。很多优秀的算法都跨越了多种设计范式：</p>
<table>
<thead>
<tr>
<th>维度</th>
<th>DP 视角</th>
<th>贪心视角</th>
</tr>
</thead>
<tbody>
<tr>
<td>决策依据</td>
<td>$dp[i] = \max(x, pre + x)$</td>
<td>如果 $pre &gt; 0$ 则保留，否则丢弃</td>
</tr>
<tr>
<td>状态</td>
<td>以 $i$ 结尾的最大子数组和</td>
<td>当前的累积和</td>
</tr>
<tr>
<td>正确性来源</td>
<td>最优子结构</td>
<td>贪心选择性质</td>
</tr>
<tr>
<td>典型问法</td>
<td>"什么是以 i 结尾的最优解？"</td>
<td>"这个前缀对我有没有帮助？"</td>
</tr>
</tbody>
</table>
<p>Kadane 算法之所以能被两种范式同时解释，是因为它的<strong>贪心选择恰好也是 DP 的最优子结构</strong>——"丢弃负前缀"这个贪心动作，在 DP 的转移方程中被验证为总是最优的。</p>
<blockquote>
<p>💡 <strong>什么时候贪心和 DP 重合？</strong> 当 DP 的状态转移只涉及"取或不取"的二元选择，且其中一种选择总是被另一种支配时（如"加负数前缀"总是被"丢弃它重新开始"支配），DP 就退化为了贪心。Kadane 正是这类"DP 退化为贪心"的经典案例。</p>
</blockquote>
<h1>执行过程可视化</h1>
<p>以 <code>nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]</code> 为例，追踪 Kadane 算法的每一步决策：</p>
<table>
<thead>
<tr>
<th><code>i</code></th>
<th><code>x</code></th>
<th>前一步 <code>pre</code></th>
<th>决策</th>
<th>新 <code>pre</code></th>
<th><code>ans</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>—</td>
<td>—</td>
<td>—</td>
<td>—</td>
<td>0</td>
<td><code>-∞</code></td>
</tr>
<tr>
<td>0</td>
<td><code>-2</code></td>
<td>0 (≤0)</td>
<td>丢弃，<code>0 + (-2) = -2</code></td>
<td><code>-2</code></td>
<td><code>-2</code></td>
</tr>
<tr>
<td>1</td>
<td><code>1</code></td>
<td>-2 (≤0)</td>
<td>丢弃，<code>0 + 1 = 1</code></td>
<td><code>1</code></td>
<td><code>1</code></td>
</tr>
<tr>
<td>2</td>
<td><code>-3</code></td>
<td>1 (&gt;0)</td>
<td>保留，<code>1 + (-3) = -2</code></td>
<td><code>-2</code></td>
<td><code>1</code></td>
</tr>
<tr>
<td>3</td>
<td><code>4</code></td>
<td>-2 (≤0)</td>
<td>丢弃，<code>0 + 4 = 4</code></td>
<td><code>4</code></td>
<td><code>4</code></td>
</tr>
<tr>
<td>4</td>
<td><code>-1</code></td>
<td>4 (&gt;0)</td>
<td>保留，<code>4 + (-1) = 3</code></td>
<td><code>3</code></td>
<td><code>4</code></td>
</tr>
<tr>
<td>5</td>
<td><code>2</code></td>
<td>3 (&gt;0)</td>
<td>保留，<code>3 + 2 = 5</code></td>
<td><code>5</code></td>
<td><code>5</code></td>
</tr>
<tr>
<td>6</td>
<td><code>1</code></td>
<td>5 (&gt;0)</td>
<td>保留，<code>5 + 1 = 6</code></td>
<td><code>6</code></td>
<td><code>6</code> ✅</td>
</tr>
<tr>
<td>7</td>
<td><code>-5</code></td>
<td>6 (&gt;0)</td>
<td>保留，<code>6 + (-5) = 1</code></td>
<td><code>1</code></td>
<td><code>6</code></td>
</tr>
<tr>
<td>8</td>
<td><code>4</code></td>
<td>1 (&gt;0)</td>
<td>保留，<code>1 + 4 = 5</code></td>
<td><code>5</code></td>
<td><code>6</code></td>
</tr>
</tbody>
</table>
<p>注意 $i = 0, 1, 3$ 三个位置都触发了"丢弃"：</p>
<ul>
<li>$i=0$：前面是 0（等效于没有前缀），取 <code>-2</code>。</li>
<li>$i=1$：<code>pre = -2</code>，是负数，丢弃前面的 <code>[-2]</code>，从 <code>1</code> 重新开始。</li>
<li>$i=3$：<code>pre = -2</code>，是负数，丢弃前面的 <code>[1, -3]</code>，从 <code>4</code> 重新开始。而 $i=4$ 到 $i=6$ 连续保留，构建出最优解 $[4, -1, 2, 1]$（和为 6）。</li>
</ul>
<blockquote>
<p>💡 <strong>这个表揭示了 Kadane 算法的运作规律</strong>："丢弃"多发生在遇到<strong>负数前缀</strong>或<strong>数组开头</strong>的地方。一旦 <code>pre</code> 转正，算法就会贪婪地一直累加（即使遇到负数也加，因为正前缀总体上还是赚的），直到前缀和再次归零或转负。</p>
</blockquote>
<h1>变体与扩展</h1>
<h2>记录子数组本身</h2>
<p>如果不仅要求和，还要返回具体的子数组怎么办？只需追踪起止索引：</p>
<pre><code>def max_subarray_with_indices(nums):
    ans = float('-inf')
    pre = 0
    start = end = 0        # 最优子数组的起止位置
    cur_start = 0           # 当前 pre 的起点

    for i, x in enumerate(nums):
        if pre &lt;= 0:
            pre = x
            cur_start = i   # 丢弃前缀，重新标记起点
        else:
            pre += x

        if pre &gt; ans:
            ans = pre
            start = cur_start
            end = i

    return nums[start:end + 1], ans
</code></pre>
<blockquote>
<p>💡 <strong>技巧</strong>：每次 <code>pre &lt;= 0</code> 触发"丢弃"时，同步更新 <code>cur_start</code> 为当前位置。当 <code>pre</code> 刷新历史最高记录时，将 <code>cur_start</code> 封存到 <code>start</code>。这样在 $O(1)$ 额外空间内就能追踪子数组边界。</p>
</blockquote>
<h2>乘积最大子数组</h2>
<p>将加法换成乘法后，负数的存在让问题变得更有趣——两个负数相乘会变成正数，所以<strong>当前的负数可能是未来的宝藏</strong>。这个变体需要同时维护"以 $i$ 结尾的最大乘积"和"以 $i$ 结尾的最小乘积"两个状态。详见 <a href="/posts/hot100/">乘积最大子数组题解</a>。</p>
<h2>环形子数组的最大和</h2>
<p>如果数组是环形的（首尾相连），最大子数组可能跨越数组两端。诀窍在于：<strong>环形最大 = max(普通最大, 总和 - 普通最小)</strong>。因为跨越两端的子数组，等价于"挖掉中间一段最小的"。详见 <a href="https://leetcode.cn/problems/maximum-circular-subarray/">环形子数组的最大和</a>。</p>
<h2>线段树维护区间最大子段和</h2>
<p>当数组需要频繁修改，并且每次修改后要快速回答"整个数组的最大子段和是多少"时，Kadane 的 $O(n)$ 单次查询就不够用了。这时需要<strong>线段树</strong>来维护区间信息——而线段树节点的合并操作，正是 Kadane 算法分治视角的推广。</p>
<h1>总结</h1>
<p>Kadane 算法虽然只有短短几行代码，但它身上凝聚了几个非常重要的算法设计思想：</p>
<ol>
<li><strong>强制结尾的 DP 状态定义</strong>：$dp[i] = \text{以 i 结尾的最优解}$，这是子数组类 DP 的万能钥匙。</li>
<li><strong>空间优化的来源识别</strong>：$dp[i]$ 只依赖 $dp[i-1]$ → 滚动变量 → $O(1)$ 空间。</li>
<li><strong>DP 退化为贪心</strong>：当状态转移中的一种选择被另一种严格支配时，DP 可以简化为贪心决策。</li>
<li><strong>分治视角</strong>：跨越中点的最大和 = 左半后缀最大 + 右半前缀最大，这直接导向线段树的节点合并。</li>
</ol>
<blockquote>
<p>💡 <strong>推荐练习顺序</strong>：最大子数组和（本题）→ 乘积最大子数组 → 环形子数组的最大和 → 线段树区间最大子段和。这个序列将 Kadane 算法的思想逐步泛化，覆盖了 DP、贪心、分治、线段树四个领域。</p>
</blockquote>
<p>掌握了 Kadane 算法，你掌握的不仅仅是一道题，而是一整套处理"子数组最值"问题的思维框架。</p>
]]></content>
    <author><name>Hygen</name></author>
    <category term="DSA"/>
  </entry>
  <entry>
    <title>包含所有三种字符的子字符串数目</title>
    <link href="https://www.hygen.red/posts/dsa/solution/014%E5%8C%85%E5%90%AB%E6%89%80%E6%9C%89%E4%B8%89%E7%A7%8D%E5%AD%97%E7%AC%A6%E7%9A%84%E5%AD%90%E5%AD%97%E7%AC%A6%E4%B8%B2%E6%95%B0%E7%9B%AE/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/dsa/solution/014%E5%8C%85%E5%90%AB%E6%89%80%E6%9C%89%E4%B8%89%E7%A7%8D%E5%AD%97%E7%AC%A6%E7%9A%84%E5%AD%90%E5%AD%97%E7%AC%A6%E4%B8%B2%E6%95%B0%E7%9B%AE/</id>
    <published>2026-06-30T00:00:00.000Z</published>
    <updated>2026-06-30T00:00:00.000Z</updated>
    <summary>LeetCode-1358</summary>
    <content type="html"><![CDATA[<h1>题目描述</h1>
<p><a href="https://leetcode.cn/problems/number-of-substrings-containing-all-three-characters/description/?envType=daily-question&amp;envId=2026-06-30">题目链接</a></p>
<p>给你一个字符串 <code>s</code>，它<strong>只包含</strong>三种字符 <code>'a'</code>、<code>'b'</code> 和 <code>'c'</code>。</p>
<p>请你返回 <code>s</code> 中<strong>至少包含</strong> <code>'a'</code>、<code>'b'</code> 和 <code>'c'</code> 各至少一个的子字符串数目。</p>
<h2>输入</h2>
<p>只包含字符 <code>'a'</code>、<code>'b'</code>、<code>'c'</code> 的字符串 <code>s</code></p>
<h2>输出</h2>
<p>满足条件的子字符串数目（整数）</p>
<h2>约束条件</h2>
<ol>
<li><code>3 &lt;= s.length &lt;= 5 * 10^4</code></li>
<li><code>s</code> 仅由 <code>'a'</code>、<code>'b'</code> 和 <code>'c'</code> 组成</li>
</ol>
<h2>示例1</h2>
<pre><code>input: s = "abcabc"
output: 10
</code></pre>
<p>解释：满足条件的子字符串有：<code>"abc"</code>, <code>"abca"</code>, <code>"abcab"</code>, <code>"abcabc"</code>, <code>"bca"</code>, <code>"bcab"</code>, <code>"bcabc"</code>, <code>"cab"</code>, <code>"cabc"</code> 以及再次出现的 <code>"abc"</code>（起始位置不同）。</p>
<h2>示例2</h2>
<pre><code>input: s = "aaacb"
output: 3
</code></pre>
<p>解释：满足条件的子字符串有：<code>"aaacb"</code>、<code>"aacb"</code> 和 <code>"acb"</code>。</p>
<h2>示例3</h2>
<pre><code>input: s = "abc"
output: 1
</code></pre>
<h1>解题思路</h1>
<p>题目要求统计<strong>至少包含</strong> <code>'a'</code>、<code>'b'</code>、<code>'c'</code> 各一个的子字符串数量。字符串仅由这三种字符构成，这意味着一旦某个子串"集齐"了三种字符，继续向右扩展永远不会失去它们——字符只会增多，不会消失。这个单调性是后续优化的关键。</p>
<h2>从暴力枚举出发</h2>
<p>最直接的想法：枚举所有可能的起点 <code>i</code> 和终点 <code>j</code>，逐一检查子串 <code>s[i:j+1]</code> 是否包含全部三种字符，若包含则计数器加一：</p>
<pre><code>class Solution:
    def numberOfSubstrings(self, s: str) -&gt; int:
        n = len(s)
        ans = 0
        for i in range(n):
            cnt = {'a': 0, 'b': 0, 'c': 0}
            for j in range(i, n):
                cnt[s[j]] += 1
                if cnt['a'] and cnt['b'] and cnt['c']:
                    ans += 1
        return ans
</code></pre>
<p>这种做法时间复杂度 $O(n^2)$，在 $n$ 可达 $5 \times 10^4$ 时会超时。但暴力解揭示了一个重要性质：<strong>对固定的起点 <code>i</code>，一旦在某个位置 <code>j</code> 首次集齐三种字符，那么 <code>j</code> 之后的所有位置也都集齐了</strong>。</p>
<h2>利用单调性：一次计数多个子串</h2>
<p>对于固定的左边界 <code>j</code>，设第一个使 <code>[j, i]</code> 满足条件的最小右边界为 <code>i</code>。由于字符串只含 <code>'a'</code>、<code>'b'</code>、<code>'c'</code>，窗口内的字符计数<strong>只增不减</strong>（或者说，向右扩展时已经拥有的字符不会丢失），因此对于任意 <code>k &gt;= i</code>，子串 <code>[j, k]</code> 也一定满足条件。</p>
<p>也就是说，只要找到了最小的 <code>i</code>，右边界可以是 <code>i, i+1, ..., n-1</code>，一共有 $n - i$ 个合法子串。我们不必逐个枚举它们，直接累加即可。</p>
<p>剩下的问题是：如何高效地找到每个 <code>j</code> 对应的最小 <code>i</code>？</p>
<h2>滑动窗口：左右指针各走一遍</h2>
<p>用两个指针维护一个窗口 <code>[j, i]</code>：</p>
<ul>
<li><code>i</code>：右边界，逐字符向右扩展，将 <code>s[i]</code> 加入计数</li>
<li><code>j</code>：左边界，当窗口内三种字符的计数都 $\ge 1$ 时，说明找到了最小 <code>i</code>（对当前 <code>j</code> 而言）</li>
</ul>
<p>当窗口满足条件时，做两件事：</p>
<ol>
<li><strong>计数</strong>：将 $n - i$ 累加到答案（当前 <code>j</code> 配上 <code>i</code> 到 $n-1$ 的任意右边界都合法）</li>
<li><strong>收缩</strong>：移除 <code>s[j]</code>，<code>j += 1</code>，尝试用更短的窗口看看是否仍满足条件——这相当于为下一个 <code>j</code> 寻找它的最小 <code>i</code></li>
</ol>
<pre><code>from collections import defaultdict

class Solution:
    def numberOfSubstrings(self, s: str) -&gt; int:
        n = len(s)
        ans = j = 0
        d = defaultdict(int)

        for i, c in enumerate(s):
            d[c] += 1
            while d['a'] and d['b'] and d['c']:
                ans += n - i
                d[s[j]] -= 1
                j += 1

        return ans
</code></pre>
<p>以 <code>s = "abcabc"</code> 为例跟踪执行过程：</p>
<table>
<thead>
<tr>
<th><code>i</code></th>
<th><code>c</code></th>
<th>窗口内计数 <code>{a,b,c}</code></th>
<th>满足条件？</th>
<th>操作</th>
<th><code>ans</code> 变化</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td><code>a</code></td>
<td>{1,0,0}</td>
<td>❌</td>
<td>—</td>
<td>0</td>
</tr>
<tr>
<td>1</td>
<td><code>b</code></td>
<td>{1,1,0}</td>
<td>❌</td>
<td>—</td>
<td>0</td>
</tr>
<tr>
<td>2</td>
<td><code>c</code></td>
<td>{1,1,1}</td>
<td>✅</td>
<td><code>ans += 6-2=4</code>, 移除 <code>s[0]=a</code>, <code>j=1</code></td>
<td>4</td>
</tr>
<tr>
<td></td>
<td></td>
<td>{0,1,1}</td>
<td>❌</td>
<td>退出 while</td>
<td>4</td>
</tr>
<tr>
<td>3</td>
<td><code>a</code></td>
<td>{1,1,1}</td>
<td>✅</td>
<td><code>ans += 6-3=3</code>, 移除 <code>s[1]=b</code>, <code>j=2</code></td>
<td>7</td>
</tr>
<tr>
<td></td>
<td></td>
<td>{1,0,1}</td>
<td>❌</td>
<td>退出 while</td>
<td>7</td>
</tr>
<tr>
<td>4</td>
<td><code>b</code></td>
<td>{1,1,1}</td>
<td>✅</td>
<td><code>ans += 6-4=2</code>, 移除 <code>s[2]=c</code>, <code>j=3</code></td>
<td>9</td>
</tr>
<tr>
<td></td>
<td></td>
<td>{0,1,1}</td>
<td>❌</td>
<td>退出 while</td>
<td>9</td>
</tr>
<tr>
<td>5</td>
<td><code>c</code></td>
<td>{1,1,1}</td>
<td>✅</td>
<td><code>ans += 6-5=1</code>, 移除 <code>s[3]=a</code>, <code>j=4</code></td>
<td>10</td>
</tr>
<tr>
<td></td>
<td></td>
<td>{0,1,1}</td>
<td>❌</td>
<td>退出 while</td>
<td>10</td>
</tr>
</tbody>
</table>
<p>最终答案为 <code>10</code>，与预期一致。</p>
<p>表中可以观察到：<code>i</code> 从未回退，<code>j</code> 也只增不减——每个字符最多被 <code>i</code> 加入一次、被 <code>j</code> 移出一次，因此内层 <code>while</code> 的总执行次数也是 $O(n)$。</p>
<h2>复杂度</h2>
<ul>
<li>时间复杂度：$O(n)$，两个指针各移动 $n$ 次</li>
<li>空间复杂度：$O(1)$，哈希表仅存储三种字符的计数</li>
</ul>
<h1>解题思路（记录最近位置）</h1>
<p>滑动窗口的核心思路是"对每个 <code>j</code> 找最小 <code>i</code>"。换个角度，我们可以<strong>对每个 <code>i</code> 找最大的 <code>j</code></strong>。</p>
<p>维护三个变量 <code>last_a</code>、<code>last_b</code>、<code>last_c</code>，分别记录字符 <code>'a'</code>、<code>'b'</code>、<code>'c'</code> 最近一次出现的下标（初始为 <code>-1</code>）。</p>
<p>当遍历到位置 <code>i</code> 时，以 <code>i</code> 为<strong>右端点</strong>的合法子串，其左端点 $j$ 必须满足：$j \le \min(last_a, last_b, last_c)$——因为 $(j, i]$ 需要包含三种字符，左端点必须<strong>不晚于</strong>三种字符中最早出现者的位置。</p>
<p>由于 $j$ 可以从 $0$ 取到 $\min(last_a, last_b, last_c)$，共有 $\min(last_a, last_b, last_c) + 1$ 个（<code>+1</code> 是因为 $j=0$ 也算一个）。</p>
<pre><code>class Solution:
    def numberOfSubstrings(self, s: str) -&gt; int:
        ans = 0
        last = {'a': -1, 'b': -1, 'c': -1}
        for i, c in enumerate(s):
            last[c] = i
            ans += min(last['a'], last['b'], last['c']) + 1
        return ans
</code></pre>
<p>以 <code>s = "abcabc"</code> 跟踪：</p>
<table>
<thead>
<tr>
<th><code>i</code></th>
<th><code>c</code></th>
<th><code>last</code></th>
<th><code>min(last)+1</code></th>
<th><code>ans</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td><code>a</code></td>
<td>{a:0, b:-1, c:-1}</td>
<td>0</td>
<td>0</td>
</tr>
<tr>
<td>1</td>
<td><code>b</code></td>
<td>{a:0, b:1, c:-1}</td>
<td>0</td>
<td>0</td>
</tr>
<tr>
<td>2</td>
<td><code>c</code></td>
<td>{a:0, b:1, c:2}</td>
<td>1</td>
<td>1</td>
</tr>
<tr>
<td>3</td>
<td><code>a</code></td>
<td>{a:3, b:1, c:2}</td>
<td>2</td>
<td>3</td>
</tr>
<tr>
<td>4</td>
<td><code>b</code></td>
<td>{a:3, b:4, c:2}</td>
<td>3</td>
<td>6</td>
</tr>
<tr>
<td>5</td>
<td><code>c</code></td>
<td>{a:3, b:4, c:5}</td>
<td>4</td>
<td>10</td>
</tr>
</tbody>
</table>
<p>同样得到 <code>10</code>。</p>
<p>这个写法的代码极简，但思维跳跃较大——需要理解"以 <code>i</code> 结尾的子串数量 = 最早出现位置 + 1"这个关系。面试中建议两种都掌握，滑动窗口更直观，最近位置更优雅。</p>
<table>
<thead>
<tr>
<th>对比项</th>
<th>滑动窗口 (<code>ans += n - i</code>)</th>
<th>最近位置 (<code>ans += min(last) + 1</code>)</th>
</tr>
</thead>
<tbody>
<tr>
<td>时间</td>
<td>$O(n)$</td>
<td>$O(n)$</td>
</tr>
<tr>
<td>空间</td>
<td>$O(1)$</td>
<td>$O(1)$</td>
</tr>
<tr>
<td>代码行数</td>
<td>13 行</td>
<td>7 行</td>
</tr>
<tr>
<td>直观程度</td>
<td>较直观</td>
<td>需要一点推导</td>
</tr>
<tr>
<td>适合场景</td>
<td>需要明确维护窗口时</td>
<td>追求极简代码时</td>
</tr>
</tbody>
</table>
<h1>代码优化</h1>
<p>两种 $O(n)$ 算法都已经是最优——至少需要遍历一次字符串。以下优化聚焦于代码细节。</p>
<h2>滑动窗口：用普通字典替代 <code>defaultdict</code></h2>
<p>本题只需三个键 <code>'a'</code>、<code>'b'</code>、<code>'c'</code>，初始化时直接给出初始值可以省去 <code>collections</code> 导入：</p>
<pre><code>class Solution:
    def numberOfSubstrings(self, s: str) -&gt; int:
        n = len(s)
        ans = j = 0
        d = {'a': 0, 'b': 0, 'c': 0}

        for i, c in enumerate(s):
            d[c] += 1
            while d['a'] and d['b'] and d['c']:
                ans += n - i
                d[s[j]] -= 1
                j += 1

        return ans
</code></pre>
<p>写法和 <code>defaultdict</code> 完全等价，但减少了一次 <code>import</code>。</p>
<h2>最近位置：用三个独立变量</h2>
<p><code>min(last['a'], last['b'], last['c'])</code> 每次都要做三次字典查找和两次 <code>min</code> 比较。用三个独立整型变量可以减少字典开销（虽然在这个数据规模下差异微乎其微）：</p>
<pre><code>class Solution:
    def numberOfSubstrings(self, s: str) -&gt; int:
        ans = 0
        last_a = last_b = last_c = -1
        for i, c in enumerate(s):
            if c == 'a':
                last_a = i
            elif c == 'b':
                last_b = i
            else:
                last_c = i
            ans += min(last_a, last_b, last_c) + 1
        return ans
</code></pre>
<p>用 <code>if/elif/else</code> 取代字典赋值，常数更小；<code>min</code> 三个独立变量也比字典取值略快。在算法竞赛或极致性能场景下可选用此写法，但日常使用中字典版本的可读性更好。</p>
]]></content>
    <author><name>Hygen</name></author>
    <category term="Algorithm and Structure"/>
  </entry>
  <entry>
    <title>最小覆盖子串</title>
    <link href="https://www.hygen.red/posts/hot100/04-%E5%AD%90%E4%B8%B2/012%E6%9C%80%E5%B0%8F%E8%A6%86%E7%9B%96%E5%AD%90%E4%B8%B2/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/hot100/04-%E5%AD%90%E4%B8%B2/012%E6%9C%80%E5%B0%8F%E8%A6%86%E7%9B%96%E5%AD%90%E4%B8%B2/</id>
    <published>2026-06-30T00:00:00.000Z</published>
    <updated>2026-06-30T00:00:00.000Z</updated>
    <summary>Leetcode-76</summary>
    <content type="html"><![CDATA[<h1>题目描述</h1>
<p><a href="https://leetcode.cn/problems/minimum-window-substring/description/?envType=study-plan-v2&amp;envId=top-100-liked">题目链接</a></p>
<p>给你一个字符串 <code>s</code>、一个字符串 <code>t</code>。返回 <code>s</code> 中涵盖 <code>t</code> 所有字符的最小子串。如果 <code>s</code> 中不存在涵盖 <code>t</code> 所有字符的子串，则返回空字符串 <code>""</code>。</p>
<p><strong>注意：</strong></p>
<ul>
<li>对于 <code>t</code> 中重复字符，我们寻找的子字符串中该字符数量必须不少于 <code>t</code> 中该字符数量。</li>
<li>如果 <code>s</code> 中存在这样的子串，我们保证它是唯一的答案。</li>
</ul>
<h2>输入</h2>
<p>字符串 <code>s</code> 和 <code>t</code></p>
<h2>输出</h2>
<p><code>s</code> 中涵盖 <code>t</code> 所有字符的最小子串；若不存在则返回 <code>""</code></p>
<h2>约束条件</h2>
<ol>
<li><code>m == s.length</code></li>
<li><code>n == t.length</code></li>
<li><code>1 &lt;= m, n &lt;= 10^5</code></li>
<li><code>s</code> 和 <code>t</code> 由英文字母组成（大小写均可）</li>
</ol>
<h1>进阶约束</h1>
<p>你能设计一个在 $O(m + n)$ 时间内解决此问题的算法吗？</p>
<h2>示例1</h2>
<pre><code>输入：s = "ADOBECODEBANC", t = "ABC"
输出："BANC"
</code></pre>
<p>解释：最小覆盖子串 <code>"BANC"</code> 包含来自字符串 <code>t</code> 的 <code>'A'</code>、<code>'B'</code> 和 <code>'C'</code>。</p>
<h2>示例2</h2>
<pre><code>输入：s = "a", t = "a"
输出："a"
</code></pre>
<p>解释：整个字符串 <code>s</code> 就是最小覆盖子串。</p>
<h2>示例3</h2>
<pre><code>输入：s = "a", t = "aa"
输出：""
</code></pre>
<p>解释：<code>t</code> 中两个字符 <code>'a'</code> 均应包含在 <code>s</code> 的子串中，因此没有符合条件的子字符串，返回空字符串。</p>
<h1>解题思路</h1>
<p>题目要求在 <code>s</code> 中找一个最短的连续子串，使得该子串包含 <code>t</code> 中的所有字符，且每个字符的出现次数不少于 <code>t</code> 中的次数。这与之前做过的「找到字符串中所有字母异位词」有相似之处——都是在滑动窗口中维护字符频次。但这里 <code>t</code> 的字符可以在子串中<strong>分散出现</strong>（中间夹着无关字符），且窗口长度不固定，我们需要找到最短的合法窗口。</p>
<p>核心思路：右指针 <code>i</code> 不断扩展窗口，每当窗口内的字符频次满足 <code>t</code> 的要求时，尝试从左边收缩窗口以找到<strong>当前右边界下</strong>的最短合法窗口，同时更新全局最优解。</p>
<h2>从暴力枚举出发</h2>
<p>最朴素的做法：枚举 <code>s</code> 的所有子串（$O(m^2)$ 个），对每个子串检查是否包含 <code>t</code> 的所有字符（$O(m)$），总体 $O(m^3)$。对于 $m \le 10^5$ 的数据规模，这显然不可行。</p>
<p>稍作优化：枚举起点 <code>i</code> 后，向右扩展终点，同时维护窗口内各字符的计数。当计数满足 <code>t</code> 的要求时，记录当前子串长度，然后尝试下一个起点。这样减少到 $O(m^2)$，但仍然不够快：</p>
<pre><code>from collections import Counter

class Solution:
    def minWindow(self, s: str, t: str) -&gt; str:
        ct = Counter(t)
        res = ''
        for i in range(len(s)):
            cs = Counter()
            for j in range(i, len(s)):
                cs[s[j]] += 1
                # 检查当前窗口是否覆盖 t
                ok = True
                for ch in ct:
                    if cs[ch] &lt; ct[ch]:
                        ok = False
                        break
                if ok and (res == '' or j - i + 1 &lt; len(res)):
                    res = s[i:j + 1]
        return res
</code></pre>
<p><strong>瓶颈</strong>：每个起点都重新建一个 <code>Counter</code> 并从零开始累加，相邻起点之间的窗口信息完全没有复用。能否让左右边界各自只向前移动，不重复计算？</p>
<h2>滑动窗口：<code>cnt</code> 计数 + 超频左缩</h2>
<p>回顾「无重复字符的最长子串」和「找到字符串中所有字母异位词」中的滑动窗口套路：<strong>右指针扩展、左指针在条件不合法时收缩</strong>。本题可以延续这一思路，关键在于如何高效判断「当前窗口是否覆盖了 <code>t</code>」。</p>
<p>维护以下状态：</p>
<ul>
<li><code>ct = Counter(t)</code>：<code>t</code> 中各字符的目标频次。</li>
<li><code>cs = defaultdict(int)</code>：当前窗口内各字符的实际频次。</li>
<li><code>cnt</code>：当前窗口内有<strong>多少种字符</strong>的频次已经达到目标值（注意：一个字符达到目标后，即使再多出现，<code>cnt</code> 也不会增加，因为该字符已经"达标"）。</li>
<li><code>j</code>：窗口左边界。</li>
</ul>
<p>算法流程：</p>
<ol>
<li>右指针 <code>i</code> 遍历 <code>s</code>，将 <code>s[i]</code> 加入窗口：
<ul>
<li>如果加入前 <code>cs[c] &lt; ct[c]</code>，说明这个字符的加入让我们朝着目标前进了一步，<code>cnt += 1</code>。</li>
<li><code>cs[c] += 1</code>。</li>
</ul>
</li>
<li>窗口可能包含了<strong>多余</strong>的字符——左侧某些字符的出现次数超过了 <code>t</code> 中的要求。此时不断右移左指针 <code>j</code>，将 <code>cs[s[j]] -= 1</code>，直到 <code>cs[s[j]]</code> 不再超过 <code>ct[s[j]]</code>。</li>
<li>当 <code>cnt == len(t)</code> 时（所有字符类型都已达标），当前窗口是一个合法覆盖。检查窗口长度是否比已有最优解更短，若是则更新 <code>res</code>。</li>
</ol>
<pre><code>from collections import Counter, defaultdict

class Solution:
    def minWindow(self, s: str, t: str) -&gt; str:
        cs = defaultdict(int)
        ct = Counter(t)
        cnt = j = 0
        res = ''

        for i, c in enumerate(s):
            if cs[c] &lt; ct[c]:
                cnt += 1
            cs[c] += 1
            while j &lt; i and cs[s[j]] &gt; ct[s[j]]:
                cs[s[j]] -= 1
                j += 1
            if cnt == len(t) and (res == '' or i - j + 1 &lt; len(res)):
                res = s[j: i + 1]

        return res
</code></pre>
<h3>关键细节解析</h3>
<p><strong>为什么用 <code>cnt</code> 而不是直接比较两个字典？</strong></p>
<p>每次检查 <code>cs</code> 是否覆盖 <code>ct</code> 需要遍历 <code>ct</code> 的所有键，最坏 $O(|\Sigma|)$（字符集大小）。虽然对英文字母来说是常数，但有了 <code>cnt</code>，判断合法性变成 $O(1)$——只需检查 <code>cnt == len(t)</code>。<code>cnt</code> 的维护也非常轻量：只在新字符"从不足到达标"的瞬间 <code>+1</code>，之后该字符再多出现也不会重复计数。</p>
<p><strong><code>while</code> 循环为什么是 <code>cs[s[j]] &gt; ct[s[j]]</code>？</strong></p>
<p>当左侧字符在窗口中的出现次数<strong>超过</strong>目标次数时，把它丢掉不会影响窗口的合法性——因为丢掉之后该字符的计数仍然 ≥ 目标值。但如果 <code>cs[s[j]] == ct[s[j]]</code>，丢掉它就会让该字符从达标变成不达标，窗口不再合法。因此 <code>while</code> 只在"多了"的时候收缩，保留恰好达标的左边界。</p>
<p><strong><code>cnt == len(t)</code> 的含义</strong></p>
<p><code>cnt</code> 记录的是「窗口中有多少个字符对覆盖 <code>t</code> 做出了贡献」。对于 <code>t = "ABC"</code>，需要 3 个字符各贡献一次，<code>cnt</code> 达到 3 即表示三个字符均已出现。如果 <code>t = "AAB"</code>，<code>len(t) = 3</code>，<code>cnt</code> 的累积过程为：第一个 <code>A</code> 使 <code>cnt=1</code>，第二个 <code>A</code> 使 <code>cnt=2</code>，<code>B</code> 使 <code>cnt=3</code>。此时 <code>cnt == len(t)</code>，表示 <code>t</code> 中的每一个字符都在窗口中被匹配上了。</p>
<h3>执行过程追踪</h3>
<p>以 <code>s = "ADOBECODEBANC"</code>，<code>t = "ABC"</code> 为例，追踪算法的关键步骤：</p>
<table>
<thead>
<tr>
<th><code>i</code></th>
<th><code>c</code></th>
<th><code>cnt</code> 变化</th>
<th><code>cs</code>（关键变化）</th>
<th><code>while</code> 收缩</th>
<th><code>j</code></th>
<th>窗口 <code>[j,i]</code></th>
<th><code>cnt==3?</code></th>
<th><code>res</code> 更新</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td><code>A</code></td>
<td><code>0→1</code></td>
<td><code>A:1</code></td>
<td>—</td>
<td>0</td>
<td><code>[0,0]="A"</code></td>
<td>否</td>
<td>—</td>
</tr>
<tr>
<td>1</td>
<td><code>D</code></td>
<td>—</td>
<td><code>D:1</code></td>
<td>—</td>
<td>0</td>
<td><code>[0,1]="AD"</code></td>
<td>否</td>
<td>—</td>
</tr>
<tr>
<td>2</td>
<td><code>O</code></td>
<td>—</td>
<td><code>O:1</code></td>
<td>—</td>
<td>0</td>
<td><code>[0,2]="ADO"</code></td>
<td>否</td>
<td>—</td>
</tr>
<tr>
<td>3</td>
<td><code>B</code></td>
<td><code>1→2</code></td>
<td><code>B:1</code></td>
<td>—</td>
<td>0</td>
<td><code>[0,3]="ADOB"</code></td>
<td>否</td>
<td>—</td>
</tr>
<tr>
<td>4</td>
<td><code>E</code></td>
<td>—</td>
<td><code>E:1</code></td>
<td>—</td>
<td>0</td>
<td><code>[0,4]="ADOBE"</code></td>
<td>否</td>
<td>—</td>
</tr>
<tr>
<td>5</td>
<td><code>C</code></td>
<td><code>2→3</code></td>
<td><code>C:1</code></td>
<td>—</td>
<td>0</td>
<td><code>[0,5]="ADOBEC"</code></td>
<td><strong>是</strong></td>
<td><code>"ADOBEC"</code> (len=6)</td>
</tr>
<tr>
<td>6</td>
<td><code>O</code></td>
<td>—</td>
<td><code>O:2</code></td>
<td>—</td>
<td>0</td>
<td><code>[0,6]</code></td>
<td>是</td>
<td>— (len=7)</td>
</tr>
<tr>
<td>7</td>
<td><code>D</code></td>
<td>—</td>
<td><code>D:2</code></td>
<td>—</td>
<td>0</td>
<td><code>[0,7]</code></td>
<td>是</td>
<td>— (len=8)</td>
</tr>
<tr>
<td>8</td>
<td><code>E</code></td>
<td>—</td>
<td><code>E:2</code></td>
<td>—</td>
<td>0</td>
<td><code>[0,8]</code></td>
<td>是</td>
<td>— (len=9)</td>
</tr>
<tr>
<td>9</td>
<td><code>B</code></td>
<td>—</td>
<td><code>B:2</code></td>
<td>—</td>
<td>0</td>
<td><code>[0,9]</code></td>
<td>是</td>
<td>— (len=10)</td>
</tr>
<tr>
<td>10</td>
<td><code>A</code></td>
<td>—</td>
<td><code>A:2</code></td>
<td><code>A:2→1, D:2→1, O:2→1, B:2→1, E:2→1</code>, j 到 5</td>
<td>5</td>
<td><code>[5,10]="CODEBA"</code></td>
<td>是</td>
<td>— (len=6)</td>
</tr>
<tr>
<td>11</td>
<td><code>N</code></td>
<td>—</td>
<td><code>N:1</code></td>
<td>—</td>
<td>5</td>
<td><code>[5,11]</code></td>
<td>是</td>
<td>— (len=7)</td>
</tr>
<tr>
<td>12</td>
<td><code>C</code></td>
<td>—</td>
<td><code>C:2</code></td>
<td><code>C:2→1, O:1→0, D:1→0, E:1→0</code>, j 到 9</td>
<td>9</td>
<td><code>[9,12]="BANC"</code></td>
<td>是</td>
<td><strong><code>"BANC"</code></strong> (len=4)</td>
</tr>
</tbody>
</table>
<p>最终答案为 <code>"BANC"</code>，长度 4。</p>
<p>表中的关键转折点在 <code>i=10</code>：第二个 <code>A</code> 进入窗口后，<code>cs['A']=2 &gt; ct['A']=1</code> 触发了 <code>while</code> 循环，左指针 <code>j</code> 连续收缩，将前面累积的多余字符全部清除，窗口从 <code>[0,10]</code> 缩至 <code>[5,10]</code>。类似地，<code>i=12</code> 时第二个 <code>C</code> 入窗，再次触发批量收缩，左指针从 5 一路跳到 9，找到了更短的合法窗口 <code>"BANC"</code>。</p>
<p><strong>为什么左指针能一口气跳过这么多字符？</strong> 因为在 <code>j=0~4</code> 的位置，每个字符的窗口计数都超过了目标计数（如 <code>D</code>、<code>O</code>、<code>E</code> 根本不在 <code>t</code> 中，计数永远是超的），所以 <code>while</code> 循环会连续执行，直到碰到一个"刚好达标"的字符才停下。这让窗口始终保持在<strong>恰好覆盖 <code>t</code> 的最小左边界</strong>。</p>
<h2>复杂度</h2>
<ul>
<li>时间复杂度：$O(m)$，右指针 <code>i</code> 遍历一次 <code>s</code>，左指针 <code>j</code> 每个字符最多被丢弃一次，每个字符入窗出窗各 $O(1)$。</li>
<li>空间复杂度：$O(|\Sigma|)$，<code>cs</code> 和 <code>ct</code> 哈希表最多存储字符集大小的键值对。本题字符集为大小写英文字母，共 52 个，可视为 $O(1)$。</li>
</ul>
<h1>解题思路（<code>need</code> 计数法）</h1>
<p>上一节用 <code>cnt</code> 统计「达标的字符贡献数」。另一种常见的实现是用 <code>need</code> 变量统计「还需要多少个字符才算覆盖」。思路对称，但收缩条件略有不同：</p>
<p>维护 <code>need = len(t)</code> 表示窗口还需要多少个有效字符。右扩时，若 <code>cs[c] &lt; ct[c]</code>，说明这个字符是"有效"的，<code>need -= 1</code>。当 <code>need == 0</code> 时窗口合法，此时从左侧收缩：只有把左侧字符丢掉<strong>之后窗口仍然合法</strong>（即 <code>cs[s[j]] &gt; ct[s[j]]</code>），才允许收缩。尝试收缩到不能再缩为止，记录结果，然后强制左移一位以寻找下一个合法窗口。</p>
<pre><code>from collections import Counter, defaultdict

class Solution:
    def minWindow(self, s: str, t: str) -&gt; str:
        cs = defaultdict(int)
        ct = Counter(t)
        need = len(t)
        j = 0
        res = ''

        for i, c in enumerate(s):
            cs[c] += 1
            if cs[c] &lt;= ct[c]:
                need -= 1

            while need == 0:
                if res == '' or i - j + 1 &lt; len(res):
                    res = s[j: i + 1]
                # 尝试收缩左侧
                cs[s[j]] -= 1
                if cs[s[j]] &lt; ct[s[j]]:
                    need += 1
                j += 1

        return res
</code></pre>
<p>两种写法的差异：</p>
<table>
<thead>
<tr>
<th>对比项</th>
<th><code>cnt</code> 计数法</th>
<th><code>need</code> 计数法</th>
</tr>
</thead>
<tbody>
<tr>
<td>计数方向</td>
<td>从 0 递增到 <code>len(t)</code></td>
<td>从 <code>len(t)</code> 递减到 0</td>
</tr>
<tr>
<td><code>while</code> 条件</td>
<td><code>cs[s[j]] &gt; ct[s[j]]</code>（超频才缩）</td>
<td><code>need == 0</code>（合法就缩）</td>
</tr>
<tr>
<td>收缩粒度</td>
<td>只缩多余字符，窗口自动维持在"恰好覆盖"的最小左边界</td>
<td>缩到刚好不合法，需要手动回退</td>
</tr>
<tr>
<td>更新 <code>res</code> 时机</td>
<td>收缩完成后检查</td>
<td>收缩循环内每步检查</td>
</tr>
<tr>
<td>可读性</td>
<td>逻辑更集中，适合滑动窗口初学者</td>
<td>更接近"最小覆盖"的直觉</td>
</tr>
</tbody>
</table>
<p>两种写法时间复杂度均为 $O(m)$，选择哪种取决于个人偏好。<code>cnt</code> 计数法的优势在于 <code>while</code> 条件简单（只判断一次字符超频），且更新 <code>res</code> 只在外层循环的一个位置；<code>need</code> 计数法在 <code>while</code> 循环内更新 <code>res</code>，但不需要在外层循环再判断。</p>
<h1>代码优化</h1>
<p>算法层面 $O(m)$ 已是最优——至少需要遍历 <code>s</code> 一次才能知道哪些字符出现。以下优化聚焦于<strong>常数因子</strong>和<strong>代码简洁度</strong>。</p>
<h2>用数组替代哈希表</h2>
<p>题目保证 <code>s</code> 和 <code>t</code> 仅由英文字母组成（大小写均可），共 52 个可能字符。可以申请长度 128 的数组（覆盖 ASCII 英文字母范围），用 <code>ord(c)</code> 做下标，比 <code>defaultdict</code> 的哈希查找更快：</p>
<pre><code>class Solution:
    def minWindow(self, s: str, t: str) -&gt; str:
        cs = [0] * 128
        ct = [0] * 128
        for c in t:
            ct[ord(c)] += 1

        cnt = j = 0
        res = ''
        target = len(t)

        for i, c in enumerate(s):
            idx = ord(c)
            if cs[idx] &lt; ct[idx]:
                cnt += 1
            cs[idx] += 1
            while j &lt; i and cs[ord(s[j])] &gt; ct[ord(s[j])]:
                cs[ord(s[j])] -= 1
                j += 1
            if cnt == target and (res == '' or i - j + 1 &lt; len(res)):
                res = s[j: i + 1]

        return res
</code></pre>
<p>数组访问比哈希表快（省去哈希计算和冲突解决），空间固定为 $O(128)$。如果确定只有小写字母（如某些变体题目），可缩至 26。</p>
<h2>提前终止优化</h2>
<p>如果右指针 <code>i</code> 已经遍历到末尾，且当前窗口已不可能比已有最优解更短，可以提前 <code>break</code>。但由于滑动窗口本身就保证每个字符只访问常数次，这个优化的实际收益不大，反而增加代码复杂度——可作为思路提及。</p>
<h2>边界情况快判</h2>
<p>添加两个快速返回判断：</p>
<pre><code>if len(s) &lt; len(t):
    return ""
if s == t:
    return s
</code></pre>
<p>这些判断在主算法之前执行，能在极端输入下省去不必要的滑动窗口过程。</p>
<h1>总结</h1>
<p>「最小覆盖子串」是滑动窗口的经典题型，也是面试中的高频题目。相比「无重复字符的最长子串」（找最长合法窗口）和「找到字符串中所有字母异位词」（找固定长度合法窗口），本题要求找<strong>最短合法窗口</strong>，是「可变长度 + 最优解追踪」的组合：</p>
<ul>
<li><strong>右扩</strong>：无条件扩展，同时维护贡献计数 <code>cnt</code>。</li>
<li><strong>左缩</strong>：当窗口包含多余字符时，持续收缩直到恰好覆盖。</li>
<li><strong>更新</strong>：收缩后检查是否更短，更新全局最优。</li>
</ul>
<p>掌握本题的 <code>cnt</code> 计数法后，类似的「覆盖」「包含」类滑动窗口题目（如至多包含 $k$ 个不同字符的最长子串、包含所有给定字符的最短子串等）都可以用同一套模板解决——只需调整 <code>cnt</code> 的判断条件和 <code>while</code> 的收缩条件即可。</p>
]]></content>
    <author><name>Hygen</name></author>
    <category term="hot100"/>
  </entry>
  <entry>
    <title>最大子数组和</title>
    <link href="https://www.hygen.red/posts/hot100/05-%E6%99%AE%E9%80%9A%E6%95%B0%E7%BB%84/013%E6%9C%80%E5%A4%A7%E5%AD%90%E6%95%B0%E7%BB%84%E5%92%8C/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/hot100/05-%E6%99%AE%E9%80%9A%E6%95%B0%E7%BB%84/013%E6%9C%80%E5%A4%A7%E5%AD%90%E6%95%B0%E7%BB%84%E5%92%8C/</id>
    <published>2026-06-30T00:00:00.000Z</published>
    <updated>2026-06-30T00:00:00.000Z</updated>
    <summary>Leetcode-53</summary>
    <content type="html"><![CDATA[<h1>题目描述</h1>
<p><a href="https://leetcode.cn/problems/maximum-subarray/description/?envType=study-plan-v2&amp;envId=top-100-liked">题目链接</a></p>
<p>给你一个整数数组 <code>nums</code>，请你找出一个具有最大和的连续子数组（子数组最少包含一个元素），返回其最大和。</p>
<p><strong>子数组</strong> 是数组中的一个连续部分。</p>
<h2>输入</h2>
<p>整数数组 <code>nums</code></p>
<h2>输出</h2>
<p>最大子数组和</p>
<h2>约束条件</h2>
<ul>
<li><code>1 &lt;= nums.length &lt;= 10^5</code></li>
<li><code>-10^4 &lt;= nums[i] &lt;= 10^4</code></li>
</ul>
<h2>进阶约束</h2>
<p>如果你已经实现复杂度为 $O(n)$ 的解法，尝试使用更为精妙的<strong>分治法</strong>求解。</p>
<h2>示例1</h2>
<pre><code>输入：nums = [-2,1,-3,4,-1,2,1,-5,4]
输出：6
</code></pre>
<p>解释：连续子数组 <code>[4,-1,2,1]</code> 的和最大，为 <code>6</code>。</p>
<h2>示例2</h2>
<pre><code>输入：nums = [1]
输出：1
</code></pre>
<h2>示例3</h2>
<pre><code>输入：nums = [5,4,-1,7,8]
输出：23
</code></pre>
<h1>解题思路</h1>
<p>这是一道经典的<strong>最大子数组和</strong>问题，也是 Kadane 算法的代表性题目。数组中的元素可能为负数，因此不能简单地从第一个正数开始一直累加——负数的出现会让累加和"缩水"，但有时为了连接后面的正数段，我们又必须"忍耐"中间的负数。</p>
<blockquote>
<p>💡 <strong>一句话概括</strong>：在遍历数组时，我们始终面临一个选择——是<strong>延续</strong>前面的累加，还是<strong>重新开始</strong>。如果前面的累加是正的，保留它；如果是负的，丢弃它。</p>
</blockquote>
<blockquote>
<p>🔗 <strong>延伸阅读</strong>：Kadane 算法的完整原理推导（DP 视角 + 贪心视角）、变体扩展和线段树推广，详见 <a href="/posts/DSA/Knowledge/kadane/">Kadane 算法详解</a>（知识点专题文章）。</p>
</blockquote>
<p>核心问题可以归结为：<strong>当遍历到某个位置时，我该延续前面的子数组，还是从这里重新开始？</strong></p>
<h2>从暴力枚举出发</h2>
<p>最直接的做法是枚举所有子数组并计算和：</p>
<pre><code>class Solution:
    def maxSubArray(self, nums: List[int]) -&gt; int:
        n = len(nums)
        ans = float('-inf')

        for i in range(n):
            cur = 0
            for j in range(i, n):
                cur += nums[j]
                ans = max(ans, cur)

        return ans
</code></pre>
<p>这段代码枚举了所有可能的起点 <code>i</code> 和终点 <code>j</code>，在 $O(n^2)$ 时间内计算出最大子数组和。</p>
<p><strong>瓶颈分析</strong>：当 $n \le 10^5$ 时，$O(n^2) = 10^{10}$ 次操作，远超时间限制。而且内部循环做了大量重复计算——以子数组 <code>[1,3]</code> 和 <code>[2,3]</code> 为例，它们的求和区间高度重叠，但暴力法每次都是从 <code>i</code> 重新累加，完全没有复用前一次的信息。</p>
<p><strong>优化方向</strong>：能否只遍历一次数组，在遍历过程中动态地做决策？</p>
<h2>动态规划：Kadane 算法</h2>
<h3>状态定义</h3>
<p>定义 $dp[i]$ 为<strong>以 $\text{nums}[i]$ 结尾的最大子数组和</strong>。注意这里的关键限制是"必须以 $\text{nums}[i]$ 结尾"——这让我们可以从子问题递推。</p>
<blockquote>
<p>💡 <strong>DP 设计技巧</strong>：当问题问"任意区间的最优值"，常见的套路是强制状态以当前位置结尾，然后在所有 $dp[i]$ 中取最大值。这让我们获得了"前一个状态 + 当前元素"的递推关系。</p>
</blockquote>
<p>对于位置 $i$，我们有两种选择：</p>
<ul>
<li><strong>自成一派</strong>：从 $\text{nums}[i]$ 开始一个全新的子数组，和为 $\text{nums}[i]$</li>
<li><strong>延续前朝</strong>：把 $\text{nums}[i]$ 接到以 $\text{nums}[i-1]$ 结尾的最大子数组后面，和为 $dp[i-1] + \text{nums}[i]$</li>
</ul>
<p>取两者中的较大值，得到状态转移方程：</p>
<p>$$dp[i] = \max(\text{nums}[i],\ dp[i-1] + \text{nums}[i])$$</p>
<p>最终答案就是 $\max\limits_{0 \le i &lt; n} dp[i]$。</p>
<h3>空间优化</h3>
<p>观察转移方程：$dp[i]$ 仅依赖于 $dp[i-1]$，我们不需要维护整个 <code>dp</code> 数组，只需一个变量 <code>pre</code> 记录 $dp[i-1]$ 即可。</p>
<p>同时注意到，$dp[i-1]$ 只有在 <strong>&gt; 0</strong> 时才对 $dp[i]$ 有正向贡献——如果 $dp[i-1] \le 0$，那么 $\max(\text{nums}[i], dp[i-1] + \text{nums}[i]) = \text{nums}[i]$，等价于「丢弃之前的所有累加」。</p>
<p>因此代码可以简化为：</p>
<pre><code>class Solution:
    def maxSubArray(self, nums: List[int]) -&gt; int:
        ans = float('-inf')
        pre = 0

        for x in nums:
            pre = max(pre, 0) + x
            ans = max(ans, pre)

        return ans
</code></pre>
<p>关键一行 <code>pre = max(pre, 0) + x</code> 的语义：</p>
<ul>
<li>如果 <code>pre &gt; 0</code>，说明前面的累积对当前元素有正向贡献 → 累加上 <code>x</code></li>
<li>如果 <code>pre \le 0</code>，说明前面的累积只会拖累当前元素 → 丢弃，从 <code>x</code> 重新开始</li>
</ul>
<blockquote>
<p>🧠 <strong>直觉理解</strong>：想象你是一个股民，每天都有盈亏。<code>pre</code> 就是你当前的<strong>累计收益</strong>，<code>max(pre, 0)</code> 就是在问自己——"我之前是赚了还是亏了？"亏了就清仓从头开始，赚了就继续持有。</p>
</blockquote>
<p>这正是 Kadane 算法最优雅的写法——将 DP 状态转移与贪心决策融为一体。</p>
<h3>执行过程追踪</h3>
<p>以 <code>nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]</code> 为例，追踪 <code>pre</code> 和 <code>ans</code> 的变化：</p>
<table>
<thead>
<tr>
<th><code>i</code></th>
<th><code>x</code></th>
<th>决策</th>
<th><code>pre</code></th>
<th><code>ans</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>—</td>
<td>—</td>
<td>—</td>
<td>0</td>
<td><code>-∞</code></td>
</tr>
<tr>
<td>0</td>
<td><code>-2</code></td>
<td><code>pre ≤ 0</code>，丢弃，<code>0 + (-2)</code></td>
<td><code>-2</code></td>
<td><code>-2</code></td>
</tr>
<tr>
<td>1</td>
<td><code>1</code></td>
<td><code>pre ≤ 0</code>，丢弃，<code>0 + 1</code></td>
<td><code>1</code></td>
<td><code>1</code></td>
</tr>
<tr>
<td>2</td>
<td><code>-3</code></td>
<td><code>pre &gt; 0</code>，保留，<code>1 + (-3)</code></td>
<td><code>-2</code></td>
<td><code>1</code></td>
</tr>
<tr>
<td>3</td>
<td><code>4</code></td>
<td><code>pre ≤ 0</code>，丢弃，<code>0 + 4</code></td>
<td><code>4</code></td>
<td><code>4</code></td>
</tr>
<tr>
<td>4</td>
<td><code>-1</code></td>
<td><code>pre &gt; 0</code>，保留，<code>4 + (-1)</code></td>
<td><code>3</code></td>
<td><code>4</code></td>
</tr>
<tr>
<td>5</td>
<td><code>2</code></td>
<td><code>pre &gt; 0</code>，保留，<code>3 + 2</code></td>
<td><code>5</code></td>
<td><code>5</code></td>
</tr>
<tr>
<td>6</td>
<td><code>1</code></td>
<td><code>pre &gt; 0</code>，保留，<code>5 + 1</code></td>
<td><code>6</code></td>
<td><code>6</code> ✅</td>
</tr>
<tr>
<td>7</td>
<td><code>-5</code></td>
<td><code>pre &gt; 0</code>，保留，<code>6 + (-5)</code></td>
<td><code>1</code></td>
<td><code>6</code></td>
</tr>
<tr>
<td>8</td>
<td><code>4</code></td>
<td><code>pre &gt; 0</code>，保留，<code>1 + 4</code></td>
<td><code>5</code></td>
<td><code>6</code></td>
</tr>
</tbody>
</table>
<p>最终 <code>ans = 6</code>，对应子数组 <code>[4, -1, 2, 1]</code>。</p>
<p>可以观察到，<code>pre</code> 在位置 <code>i=0, 1, 3</code> 处 <strong>≤ 0</strong> 时被"重置"——这恰好对应了"切断负数前缀，重新开始"的贪心决策。最终的最大值 <code>6</code> 出现在 <code>i=6</code>。</p>
<h2>复杂度</h2>
<ul>
<li><strong>时间复杂度</strong>：$O(n)$，只需遍历数组一次。</li>
<li><strong>空间复杂度</strong>：$O(1)$，仅使用 <code>pre</code> 和 <code>ans</code> 两个变量。</li>
</ul>
<p>$O(n)$ 已是最优——因为输出结果至少需要遍历一次数组才能确定。</p>
<h1>解题思路（分治法）</h1>
<p>除了 DP，题目的进阶要求也提示我们尝试分治法。分治法的思路是将数组从中间分成左右两半，那么最大子数组有三种可能的位置：</p>
<table>
<thead>
<tr>
<th>情况</th>
<th>描述</th>
<th>求解方式</th>
</tr>
</thead>
<tbody>
<tr>
<td>① 完全在左半</td>
<td>最大子数组的左右端点均 ≤ <code>mid</code></td>
<td>递归左半</td>
</tr>
<tr>
<td>② 完全在右半</td>
<td>最大子数组的左右端点均 &gt; <code>mid</code></td>
<td>递归右半</td>
</tr>
<tr>
<td>③ 跨越中点</td>
<td>左端点在左半，右端点在右半</td>
<td>从中点向两侧扫描</td>
</tr>
</tbody>
</table>
<p>前两种情况直接递归求解，第三种情况需要额外处理——从中点向左扫描求最大后缀和，从中点+1 向右扫描求最大前缀和，两者相加：</p>
<pre><code>class Solution:
    def maxSubArray(self, nums: List[int]) -&gt; int:
        def divide(l: int, r: int) -&gt; int:
            if l == r:
                return nums[l]

            mid = (l + r) // 2

            # 递归求解左右
            left_max = divide(l, mid)
            right_max = divide(mid + 1, r)

            # 跨越中点的最大和
            cur = 0
            left_cross = float('-inf')
            for i in range(mid, l - 1, -1):
                cur += nums[i]
                left_cross = max(left_cross, cur)

            cur = 0
            right_cross = float('-inf')
            for i in range(mid + 1, r + 1):
                cur += nums[i]
                right_cross = max(right_cross, cur)

            cross_max = left_cross + right_cross

            return max(left_max, right_max, cross_max)

        return divide(0, len(nums) - 1)
</code></pre>
<blockquote>
<p>💡 <strong>为什么学分治法？</strong> 虽然复杂度不如 Kadane，但分治法中"合并区间"的操作，正是<strong>线段树维护区间最大子段和</strong>的核心——当需要支持动态修改数组元素并快速查询任意区间答案时，分治 + 线段树是唯一选择。Kadane 做不到这一点。</p>
</blockquote>
<p>两种方法的对比：</p>
<table>
<thead>
<tr>
<th>对比项</th>
<th>Kadane（DP）</th>
<th>分治法</th>
</tr>
</thead>
<tbody>
<tr>
<td>时间复杂度</td>
<td>$O(n)$</td>
<td>$O(n \log n)$</td>
</tr>
<tr>
<td>空间复杂度</td>
<td>$O(1)$</td>
<td>$O(\log n)$</td>
</tr>
<tr>
<td>代码简洁度</td>
<td>⭐⭐⭐⭐⭐</td>
<td>⭐⭐⭐</td>
</tr>
<tr>
<td>思维难度</td>
<td>需理解 DP 状态定义</td>
<td>结构清晰，但跨越中点处理稍繁琐</td>
</tr>
<tr>
<td>扩展性</td>
<td>仅适合静态最大子数组和</td>
<td>可扩展为线段树维护区间信息</td>
</tr>
</tbody>
</table>
<h1>代码优化</h1>
<h2><code>max(pre, 0) + x</code> 与 <code>max(x, pre + x)</code> 的等价性</h2>
<pre><code># 写法 1：强调"丢弃负前缀"（更直观）
pre = max(pre, 0) + x

# 写法 2：强调"取或不取"（更贴近 DP 方程）
pre = max(x, pre + x)
</code></pre>
<p>写法 1 更直观地体现了贪心决策——「之前和为正则保留，为负则丢弃」。写法 2 更贴近标准 DP 状态转移方程。两种写法完全等价，按个人偏好选择。</p>
<h2>初始化细节</h2>
<pre><code>ans = float('-inf')   # 正确处理全负数数组
pre = 0
</code></pre>
<blockquote>
<p>⚠️ <strong>注意</strong>：<code>ans</code> 初始化为 <code>float('-inf')</code> 而非 <code>0</code>，确保全负数数组（如 <code>nums = [-3, -2, -1]</code>）也能正确返回 <code>-1</code>。若用 <code>0</code> 则会错误地返回 <code>0</code>（空子数组在本题中是不允许的）。</p>
</blockquote>
<p>等价的初始化方式：<code>ans = nums[0]</code>（题目保证 <code>nums.length &gt;= 1</code>），两种写法都是安全的。</p>
<h2>扩展：返回子数组本身</h2>
<p>如果题目要求返回最大子数组而不仅是最大值，可以在维持 <code>pre</code> 的同时追踪子数组的左右端点：</p>
<pre><code>class Solution:
    def maxSubArray(self, nums: List[int]) -&gt; List[int]:
        ans = float('-inf')
        pre = 0
        start = end = 0
        cur_start = 0

        for i, x in enumerate(nums):
            if pre &lt;= 0:
                pre = x
                cur_start = i      # 丢弃前缀，重新标记起点
            else:
                pre += x

            if pre &gt; ans:
                ans = pre
                start = cur_start
                end = i

        return nums[start:end + 1]
</code></pre>
<blockquote>
<p>💡 <strong>技巧</strong>：每次 <code>pre &lt;= 0</code> 触发"丢弃"时，同步更新 <code>cur_start</code> 为当前位置；当 <code>pre</code> 刷新历史记录时，将 <code>cur_start</code> 封存到 <code>start</code>。$O(1)$ 额外空间即可追踪子数组边界。</p>
</blockquote>
<h1>总结</h1>
<p>「最大子数组和」是动态规划的入门经典，它的核心在于<strong>定义以每个位置结尾的最优状态</strong>，通过一个精妙的转移方程将问题降维到 $O(n)$。</p>
<p>解决本题后，推荐按以下顺序挑战延伸题目——这条路径将 Kadane 的思想逐步泛化，覆盖 DP、贪心、分治、线段树四个领域：</p>
<ul>
<li><strong><a href="https://leetcode.cn/problems/maximum-product-subarray/">152. 乘积最大子数组</a></strong>：将加法换为乘法，负数会翻转符号——需要同时维护最大值和最小值</li>
<li><strong><a href="https://leetcode.cn/problems/maximum-circular-subarray/">918. 环形子数组的最大和</a></strong>：首尾相连，诀窍是"环形最大 = max(普通最大, 总和 - 普通最小)"</li>
<li><strong>线段树维护区间最大子段和</strong>：数组可修改时，如何用分治结构快速回答任意区间的最大子数组和？</li>
</ul>
<blockquote>
<p>🧠 <strong>核心收获</strong>：掌握了"以某位置结尾的最优状态"这一 DP 设计技巧，你就能从容应对一大类子数组问题。这不是一道题的技巧，而是一类题的思维框架。</p>
</blockquote>
]]></content>
    <author><name>Hygen</name></author>
    <category term="hot100"/>
  </entry>
  <entry>
    <title>接雨水</title>
    <link href="https://www.hygen.red/posts/hot100/02-%E5%8F%8C%E6%8C%87%E9%92%88/007%E6%8E%A5%E9%9B%A8%E6%B0%B4/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/hot100/02-%E5%8F%8C%E6%8C%87%E9%92%88/007%E6%8E%A5%E9%9B%A8%E6%B0%B4/</id>
    <published>2026-06-28T00:00:00.000Z</published>
    <updated>2026-06-28T00:00:00.000Z</updated>
    <summary>Leetcode-42</summary>
    <content type="html"><![CDATA[<h1>题目描述</h1>
<p><a href="https://leetcode.cn/problems/trapping-rain-water/description/?envType=study-plan-v2&amp;envId=top-100-liked">题目链接</a></p>
<p>给定 <code>n</code> 个非负整数表示每个宽度为 <code>1</code> 的柱子的高度图，计算按此排列的柱子，下雨之后能接多少雨水。</p>
<h2>输入</h2>
<p>整数数组 <code>height</code></p>
<h2>输出</h2>
<p>能接的雨水总量（整数）</p>
<h2>约束条件</h2>
<ol>
<li><code>n == height.length</code></li>
<li><code>1 &lt;= n &lt;= 2 * 10^4</code></li>
<li><code>0 &lt;= height[i] &lt;= 10^5</code></li>
</ol>
<h1>进阶约束</h1>
<ol>
<li>能否将时间复杂度优化到 $O(n)$？</li>
<li>能否将额外空间复杂度优化到 $O(1)$？</li>
</ol>
<h2>示例1</h2>
<pre><code>input: height = [0,1,0,2,1,0,1,3,2,1,2,1]
output: 6
</code></pre>
<p>解释：蓝色部分为接住的雨水，共 <code>6</code> 个单位。</p>
<h2>示例2</h2>
<pre><code>input: height = [4,2,0,3,2,5]
output: 9
</code></pre>
<h1>解题思路</h1>
<p>题目本质是：对每个位置，求它上方能「兜住」多少水。某一格的水位由<strong>左侧最高柱</strong>和<strong>右侧最高柱</strong>中的<strong>较小值</strong>决定，再减去自身高度：</p>
<p>$$\text{water}[i] = \max\bigl(0,\ \min(\text{leftMax}[i],\ \text{rightMax}[i]) - \text{height}[i]\bigr)$$</p>
<h2>从暴力枚举出发</h2>
<p>最直观的做法是：对每个位置 <code>i</code>，分别向左、向右扫描，求出左侧最大值和右侧最大值，再按上式累加：</p>
<pre><code>class Solution:
    def trap(self, height: List[int]) -&gt; int:
        ans = 0
        n = len(height)
        for i in range(n):
            left_max = max(height[:i + 1])
            right_max = max(height[i:])
            ans += min(left_max, right_max) - height[i]
        return ans
</code></pre>
<p>思路正确，但每个位置都要左右各扫一遍，时间复杂度 $O(n^2)$，在 $n$ 可达 $2 \times 10^4$ 时会超时。瓶颈在于<strong>重复计算</strong>左右最大值——能否预处理出来？</p>
<h2>前缀最大值：逐列算积水</h2>
<p>定义两个数组：</p>
<ul>
<li><code>pre[i]</code>：下标 <code>0</code> 到 <code>i</code>（含）中的最大高度，即位置 <code>i</code> 左侧（含自身）的最高柱</li>
<li><code>suf[i]</code>：下标 <code>i</code> 到 <code>n-1</code>（含）中的最大高度，即位置 <code>i</code> 右侧（含自身）的最高柱</li>
</ul>
<p>一次从左到右、一次从右到左即可 $O(n)$ 预处理，再逐位累加：</p>
<pre><code>class Solution:
    def trap(self, height: List[int]) -&gt; int:
        n = len(height)
        pre = [0] * n
        suf = [0] * n
        pre[0] = height[0]
        for i in range(1, n):
            pre[i] = max(pre[i - 1], height[i])
        suf[-1] = height[-1]
        for i in range(n - 2, -1, -1):
            suf[i] = max(suf[i + 1], height[i])
        return sum(min(pre[i], suf[i]) - height[i] for i in range(n))
</code></pre>
<p>时间 $O(n)$，但用了 $O(n)$ 额外数组。进阶约束要求 $O(1)$ 空间——能否在遍历过程中<strong>动态维护</strong>左右最大值，而不显式存下整个数组？</p>
<h2>双指针：只维护「有效」的一侧最大值</h2>
<p>用两个指针 <code>l</code>、<code>r</code> 分别从数组首尾向中间移动，同时维护：</p>
<ul>
<li><code>pre</code>：已处理过的<strong>左半部分</strong>的最大高度（不含当前 <code>l</code> 即将纳入的那一格之前的含义，代码里是先更新再累加）</li>
<li><code>suf</code>：已处理过的<strong>右半部分</strong>的最大高度</li>
</ul>
<p>核心观察：当 <code>height[l] &lt; height[r]</code> 时，<strong>右侧</strong>至少有一根高度为 <code>height[r]</code> 的柱子，它不低于 <code>height[l]</code>。因此对当前位置 <code>l</code> 而言，决定水位的「短板」一定在<strong>左侧</strong>——水位由 <code>pre</code>（左半已扫描部分的最大值）决定，与右侧更高细节无关。</p>
<p>反之，当 <code>height[l] &gt;= height[r]</code> 时，左侧已有足够高的墙，当前位置 <code>r</code> 的水位由 <code>suf</code> 决定。</p>
<p>以 <code>height = [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]</code> 为例，前几步如下：</p>
<table>
<thead>
<tr>
<th><code>l</code></th>
<th><code>r</code></th>
<th><code>height[l]</code></th>
<th><code>height[r]</code></th>
<th>移动侧</th>
<th><code>pre</code> / <code>suf</code></th>
<th>本次积水</th>
<th><code>ans</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>11</td>
<td>0</td>
<td>1</td>
<td>左（更矮）</td>
<td><code>pre=0</code></td>
<td>0</td>
<td>0</td>
</tr>
<tr>
<td>1</td>
<td>11</td>
<td>1</td>
<td>1</td>
<td>右（相等移右）</td>
<td><code>suf=1</code></td>
<td>0</td>
<td>0</td>
</tr>
<tr>
<td>1</td>
<td>10</td>
<td>1</td>
<td>2</td>
<td>左（更矮）</td>
<td><code>pre=1</code></td>
<td>0</td>
<td>0</td>
</tr>
<tr>
<td>2</td>
<td>10</td>
<td>0</td>
<td>2</td>
<td>左（更矮）</td>
<td><code>pre=1</code></td>
<td>1</td>
<td>1</td>
</tr>
<tr>
<td>3</td>
<td>10</td>
<td>2</td>
<td>2</td>
<td>右</td>
<td><code>suf=2</code></td>
<td>0</td>
<td>1</td>
</tr>
<tr>
<td>3</td>
<td>9</td>
<td>2</td>
<td>1</td>
<td>右（更矮）</td>
<td><code>suf=2</code></td>
<td>1</td>
<td>2</td>
</tr>
</tbody>
</table>
<p>每次移动较短（或相等时移动右）一侧，用该侧已维护的最大值减去当前高度，累加到答案。两侧指针最终相遇，所有位置的积水都算完。</p>
<p>为什么不会漏算？每当处理 <code>l</code> 时，右侧存在 <code>height[r]</code> 作为「可靠高墙」，左侧最大值 <code>pre</code> 就是该位置真实的 <code>leftMax</code>；对称地，处理 <code>r</code> 时左侧 <code>height[l]</code> 保证了 <code>suf</code> 的有效性。</p>
<h2>复杂度</h2>
<ul>
<li>时间复杂度：$O(n)$，两个指针各最多移动 $n - 1$ 次</li>
<li>空间复杂度：$O(1)$，仅维护常数个变量</li>
</ul>
<h2>代码</h2>
<pre><code>class Solution:
    def trap(self, height: List[int]) -&gt; int:
        l, r = 0, len(height) - 1
        ans = 0
        pre = suf = 0

        while l &lt; r:
            if height[l] &lt; height[r]:
                pre = max(pre, height[l])
                ans += pre - height[l]
                l += 1
            else:
                suf = max(suf, height[r])
                ans += suf - height[r]
                r -= 1

        return ans
</code></pre>
<h1>解题思路（单调栈）</h1>
<p>双指针是<strong>按列</strong>逐格累加积水；另一种视角是<strong>按层</strong>横向「填水」——当遇到一根比栈顶更高的柱子时，之前凹下去的区域可以被填满一层。</p>
<p>维护一个<strong>单调递减栈</strong> <code>st</code>，存下标，栈底到栈顶对应的高度单调不增。遍历到位置 <code>i</code>、高度 <code>h</code> 时：</p>
<ol>
<li>若 <code>h</code> 不大于栈顶高度，说明还没形成新的「盆」，将 <code>i</code> 入栈</li>
<li>若 <code>h</code> 大于栈顶高度，说明右侧出现了更高的墙，可以兜住水：
<ul>
<li>弹出栈顶作为<strong>盆底</strong> <code>bottom</code></li>
<li>若栈空，说明左侧没有墙，无法接水，<code>break</code></li>
<li>否则栈顶（弹出后新的栈顶）是<strong>左侧墙</strong>，与当前 <code>h</code>（右侧墙）共同夹住一层水</li>
<li>宽度为 <code>i - st[-1] - 1</code>，高度为 <code>min(左墙, 右墙) - bottom</code></li>
</ul>
</li>
</ol>
<p>以 <code>height = [0, 1, 0, 2, 1, 0, 1, 3]</code> 为例，当 <code>i = 7</code>、<code>h = 3</code> 时，栈中依次为下标 <code>1(高1)、2(高0)、5(高0)、6(高1)</code>。连续弹出后，会在 <code>(1, 3)</code> 之间、<code>(6, 3)</code> 之间等位置分别算出一层积水。</p>
<p>这种写法把「凹槽」拆成若干横向矩形累加，与双指针的纵向切片殊途同归。</p>
<h2>复杂度</h2>
<ul>
<li>时间复杂度：$O(n)$，每个下标最多入栈、出栈各一次</li>
<li>空间复杂度：$O(n)$，栈中最多存 $n$ 个下标</li>
</ul>
<h2>代码</h2>
<pre><code>class Solution:
    def trap(self, height: List[int]) -&gt; int:
        st = []
        ans = 0
        
        for i, h in enumerate(height):
            while st and height[st[-1]] &lt; h:
                bottom = height[st.pop()]
                if not st: break
                ans += (min(height[st[-1]], h) - bottom) * (i - st[-1] - 1)
            st.append(i)

        return ans
</code></pre>
<table>
<thead>
<tr>
<th>对比项</th>
<th>双指针</th>
<th>单调栈</th>
</tr>
</thead>
<tbody>
<tr>
<td>时间</td>
<td>$O(n)$</td>
<td>$O(n)$</td>
</tr>
<tr>
<td>空间</td>
<td>$O(1)$</td>
<td>$O(n)$</td>
</tr>
<tr>
<td>视角</td>
<td>逐列：每格水位由左右最大值决定</td>
<td>逐层：弹出盆底后横向填水</td>
</tr>
<tr>
<td>适用</td>
<td>本题最优解，代码短</td>
<td>便于扩展到接雨水 II 等二维变形</td>
</tr>
</tbody>
</table>
<p>面试和竞赛中更推荐双指针：空间更优、实现更短；单调栈则适合需要<strong>横向分层</strong>或推广到矩阵接水的场景。</p>
<h1>代码优化</h1>
<p>本题在算法层面，$O(n)$ 时间已是下界（至少要看一遍数组），$O(1)$ 空间的双指针也是最优之一。因此优化空间主要在<strong>代码简洁度</strong>与<strong>常数细节</strong>。</p>
<h2>合并 <code>pre</code> 与 <code>suf</code> 为单一变量</h2>
<p>同一时刻只有较短一侧在被处理，另一侧的最大值变量本轮不会用到。可以只保留一个 <code>mx</code>，在左右分支里分别更新，语义不变：</p>
<pre><code>class Solution:
    def trap(self, height: List[int]) -&gt; int:
        l, r = 0, len(height) - 1
        ans = mx = 0
        while l &lt; r:
            if height[l] &lt; height[r]:
                mx = max(mx, height[l])
                ans += mx - height[l]
                l += 1
            else:
                mx = max(mx, height[r])
                ans += mx - height[r]
                r -= 1
        return ans
</code></pre>
<p>注意：两侧交替处理时 <code>mx</code> 的含义会随分支切换，但<strong>每次累加前</strong>它代表的正是当前侧所需的「已扫描最大值」，与分开维护 <code>pre</code>、<code>suf</code> 等价。若更看重可读性，保留 <code>pre</code>、<code>suf</code> 两个变量更清晰。</p>
<h2>单调栈：提前缓存栈顶高度</h2>
<p>内层 <code>while</code> 中多次访问 <code>height[st[-1]]</code>，可先取出栈顶下标与高度，减少重复下标计算：</p>
<pre><code>class Solution:
    def trap(self, height: List[int]) -&gt; int:
        st = []
        ans = 0
        for i, h in enumerate(height):
            while st:
                top = st[-1]
                if height[top] &gt;= h:
                    break
                bottom = height[st.pop()]
                if not st:
                    break
                ans += (min(height[st[-1]], h) - bottom) * (i - st[-1] - 1)
            st.append(i)
        return ans
</code></pre>
<p>逻辑与原版相同，将 <code>height[st[-1]] &lt; h</code> 的判断改为显式 <code>break</code>，对部分读者更易跟进循环出口。</p>
]]></content>
    <author><name>Hygen</name></author>
    <category term="hot100"/>
  </entry>
  <entry>
    <title>无重复字符的最长子串</title>
    <link href="https://www.hygen.red/posts/hot100/03-%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/008%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/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/hot100/03-%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/008%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/</id>
    <published>2026-06-28T00:00:00.000Z</published>
    <updated>2026-06-28T00:00:00.000Z</updated>
    <summary>Leetcode-3</summary>
    <content type="html"><![CDATA[<h1>题目描述</h1>
<p><a href="https://leetcode.cn/problems/longest-substring-without-repeating-characters/?envType=study-plan-v2&amp;envId=top-100-liked">题目链接</a></p>
<p>给定一个字符串 <code>s</code>，请你找出其中不含有重复字符的<strong>最长子串</strong>的长度。</p>
<h2>输入</h2>
<p>字符串 <code>s</code></p>
<h2>输出</h2>
<p>最长无重复子串的长度（整数）</p>
<h2>约束条件</h2>
<ol>
<li><code>0 &lt;= s.length &lt;= 5 * 10^4</code></li>
<li><code>s</code> 由英文字母、数字、符号和空格组成</li>
</ol>
<h1>进阶约束</h1>
<ol>
<li>能否将时间复杂度优化到 $O(n)$？</li>
</ol>
<h2>示例1</h2>
<pre><code>input: s = "abcabcbb"
output: 3
</code></pre>
<p>解释：最长无重复子串为 <code>"abc"</code>，长度为 <code>3</code>。</p>
<h2>示例2</h2>
<pre><code>input: s = "bbbbb"
output: 1
</code></pre>
<p>解释：最长无重复子串为 <code>"b"</code>，长度为 <code>1</code>。</p>
<h2>示例3</h2>
<pre><code>input: s = "pwwkew"
output: 3
</code></pre>
<p>解释：最长无重复子串为 <code>"wke"</code>，长度为 <code>3</code>。注意 <code>"pwke"</code> 是子序列而非子串。</p>
<h1>解题思路</h1>
<p>题目要求找<strong>连续</strong>子串，且子串内每个字符最多出现一次。暴力枚举所有子串再逐个判重，在 $n$ 可达 $5 \times 10^4$ 时会超时——能否在扩展子串的同时，<strong>动态维护</strong>「当前窗口是否合法」？</p>
<h2>从暴力枚举出发</h2>
<p>最直观的做法：枚举所有起点 <code>i</code> 和终点 <code>j</code>，检查 <code>s[i:j+1]</code> 是否有重复字符：</p>
<pre><code>class Solution:
    def lengthOfLongestSubstring(self, s: str) -&gt; int:
        ans = 0
        n = len(s)
        for i in range(n):
            seen = set()
            for j in range(i, n):
                if s[j] in seen:
                    break
                seen.add(s[j])
                ans = max(ans, j - i + 1)
        return ans
</code></pre>
<p>内层遇到重复就 <code>break</code>，每个起点最多扫到第一次冲突为止。最坏情况（如全不重复）时间复杂度 $O(n^2)$，仍可能超时。瓶颈在于：<strong>不同起点之间大量重复扫描</strong>——能否让左右边界只各移动一次？</p>
<h2>滑动窗口：右扩左缩</h2>
<p>用两个指针维护一个<strong>窗口</strong> <code>[j, i]</code>（闭区间）：</p>
<ul>
<li><code>i</code>：右边界，逐字符向右扩展</li>
<li><code>j</code>：左边界，当窗口内出现重复时向右收缩</li>
</ul>
<p>再用一个集合 <code>seen</code> 记录窗口内有哪些字符。<code>i</code> 每右移一步，若 <code>s[i]</code> 不在集合中则加入并更新答案；若已存在，则不断将 <code>s[j]</code> 从集合移除并 <code>j += 1</code>，直到可以安全放入 <code>s[i]</code>：</p>
<pre><code>class Solution:
    def lengthOfLongestSubstring(self, s: str) -&gt; int:
        ans = j = 0
        seen = set()
        for i, c in enumerate(s):
            while c in seen:
                seen.remove(s[j])
                j += 1
            seen.add(c)
            ans = max(ans, i - j + 1)
        return ans
</code></pre>
<p>以 <code>s = "pwwkew"</code> 为例：</p>
<table>
<thead>
<tr>
<th><code>i</code></th>
<th><code>c</code></th>
<th>窗口 <code>[j, i]</code></th>
<th>操作</th>
<th><code>ans</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td><code>p</code></td>
<td><code>[0,0]</code></td>
<td>加入 <code>p</code></td>
<td>1</td>
</tr>
<tr>
<td>1</td>
<td><code>w</code></td>
<td><code>[0,1]</code></td>
<td>加入 <code>w</code></td>
<td>2</td>
</tr>
<tr>
<td>2</td>
<td><code>w</code></td>
<td>—</td>
<td><code>w</code> 重复，移除 <code>p</code>、<code>w</code>（<code>j</code> 到 2）</td>
<td>2</td>
</tr>
<tr>
<td>2</td>
<td><code>w</code></td>
<td><code>[2,2]</code></td>
<td>加入 <code>w</code></td>
<td>2</td>
</tr>
<tr>
<td>3</td>
<td><code>k</code></td>
<td><code>[2,3]</code></td>
<td>加入 <code>k</code></td>
<td>2</td>
</tr>
<tr>
<td>4</td>
<td><code>e</code></td>
<td><code>[2,4]</code></td>
<td>加入 <code>e</code></td>
<td>3</td>
</tr>
<tr>
<td>5</td>
<td><code>w</code></td>
<td>—</td>
<td><code>w</code> 重复，移除 <code>w</code>（<code>j</code> 到 3）</td>
<td>3</td>
</tr>
<tr>
<td>5</td>
<td><code>w</code></td>
<td><code>[3,5]</code></td>
<td>加入 <code>w</code></td>
<td>3</td>
</tr>
</tbody>
</table>
<p>最终答案为 <code>3</code>（<code>"wke"</code>）。</p>
<p><code>j</code> 只增不减，每个字符最多入窗、出窗各一次，时间复杂度 $O(n)$。</p>
<h2>滑动窗口 + 计数：用出现次数判重</h2>
<p>集合的 <code>c in seen</code> 等价于「该字符出现次数 $\ge 1$」。改用哈希表 <code>d</code> 统计窗口内各字符出现次数，逻辑可以写得更统一：</p>
<ol>
<li>右指针 <code>i</code> 每步将 <code>s[i]</code> 的计数 <code>+1</code></li>
<li>若某字符计数 $&gt; 1$，说明窗口非法，用 <code>while</code> 从左侧逐个减计数并右移 <code>j</code>，直到该字符计数回到 <code>1</code></li>
<li>窗口合法后，用 <code>i - j + 1</code> 更新答案</li>
</ol>
<pre><code>from collections import defaultdict

class Solution:
    def lengthOfLongestSubstring(self, s: str) -&gt; int:
        ans = j = 0
        d = defaultdict(int)
        for i, c in enumerate(s):
            d[c] += 1
            while d[c] &gt; 1:
                d[s[j]] -= 1
                j += 1
            ans = max(ans, i - j + 1)
        
        return ans
</code></pre>
<p>以 <code>s = "abcabcbb"</code> 为例，当 <code>i = 3</code>、<code>c = 'a'</code> 时：</p>
<table>
<thead>
<tr>
<th>步骤</th>
<th><code>d['a']</code></th>
<th><code>j</code></th>
<th>窗口</th>
<th><code>ans</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>加入 <code>a</code>（<code>i=3</code>）</td>
<td>2</td>
<td>0</td>
<td>非法</td>
<td>3</td>
</tr>
<tr>
<td><code>d[s[0]]-=1</code>，<code>j=1</code></td>
<td>1</td>
<td>1</td>
<td><code>[1,3]</code> = <code>"bca"</code></td>
<td>3</td>
</tr>
</tbody>
</table>
<p>此时 <code>'a'</code> 计数恢复为 <code>1</code>，窗口合法，长度为 <code>3</code>。</p>
<p>与集合写法相比，计数写法把「是否存在」推广为「出现几次」，后续遇到<strong>允许字符出现 $k$ 次</strong>的滑动窗口变形题时可以直接复用同一套框架。</p>
<h2>复杂度</h2>
<ul>
<li>时间复杂度：$O(n)$，<code>i</code> 与 <code>j</code> 各最多移动 $n$ 次</li>
<li>空间复杂度：$O(|\Sigma|)$，哈希表最多存字符集大小个键；字符集有限时视为 $O(1)$</li>
</ul>
<h1>解题思路（直接跳转左边界）</h1>
<p>计数写法中，<code>while d[c] &gt; 1</code> 会<strong>逐字符</strong>收缩左边界。其实重复发生时，左边界不必慢慢挪——只需跳到<strong>上一次该字符出现位置的下一格</strong>。</p>
<p>维护 <code>last[c]</code> 为字符 <code>c</code> 最近一次出现的下标。当在位置 <code>i</code> 再次遇到 <code>c</code> 时，若 <code>last[c] &gt;= j</code>（说明 <code>c</code> 仍在当前窗口内），则直接令 <code>j = last[c] + 1</code>；否则 <code>j</code> 不动（旧位置已被之前的收缩甩在窗口外）。然后更新 <code>last[c] = i</code> 并刷新答案：</p>
<pre><code>class Solution:
    def lengthOfLongestSubstring(self, s: str) -&gt; int:
        last = {}
        ans = j = 0
        for i, c in enumerate(s):
            if c in last and last[c] &gt;= j:
                j = last[c] + 1
            last[c] = i
            ans = max(ans, i - j + 1)
        return ans
</code></pre>
<p>以 <code>s = "abba"</code> 为例：当 <code>i = 3</code>、<code>c = 'a'</code> 时，<code>last['a'] = 0</code> 且 <code>0 &gt;= j</code>，直接 <code>j = 1</code>，窗口变为 <code>"ba"</code>，无需经过 <code>"bba"</code> 的中间状态。</p>
<p>两种写法时间均为 $O(n)$，但跳转写法每次循环只做常数操作，没有内层 <code>while</code>，常数更小，代码也更短。</p>
<table>
<thead>
<tr>
<th>对比项</th>
<th>计数 + 逐位收缩</th>
<th>记录下标 + 直接跳转</th>
</tr>
</thead>
<tbody>
<tr>
<td>时间</td>
<td>$O(n)$</td>
<td>$O(n)$</td>
</tr>
<tr>
<td>内层循环</td>
<td>有 <code>while</code></td>
<td>无</td>
</tr>
<tr>
<td>扩展性</td>
<td>易改为「最多出现 $k$ 次」</td>
<td>需额外维护次数</td>
</tr>
<tr>
<td>适用</td>
<td>通用滑动窗口模板</td>
<td>本题「至多 1 次」特化</td>
</tr>
</tbody>
</table>
<p>面试中若强调<strong>通用模板</strong>，推荐计数写法；若只求本题最简实现，记录下标跳转更利落。</p>
<h1>代码优化</h1>
<p>算法层面两种 $O(n)$ 思路都已最优（至少需读一遍字符串）。以下优化侧重<strong>代码简洁度</strong>与<strong>常数细节</strong>。</p>
<h2>用普通字典替代 <code>defaultdict</code></h2>
<p>本题只需「先加再减」，完全可以用 <code>dict.get(key, 0)</code> 或 <code>d[c] = d.get(c, 0) + 1</code>，少一个 <code>collections</code> 依赖：</p>
<pre><code>class Solution:
    def lengthOfLongestSubstring(self, s: str) -&gt; int:
        ans = j = 0
        d = {}
        for i, c in enumerate(s):
            d[c] = d.get(c, 0) + 1
            while d[c] &gt; 1:
                d[s[j]] -= 1
                j += 1
            ans = max(ans, i - j + 1)
        return ans
</code></pre>
<p>语义与 <code>defaultdict(int)</code> 完全一致。</p>
<h2>跳转写法：合并更新与判断</h2>
<p>记录下标版本中，<code>last[c] = i</code> 可以放在判断之前，用 <code>max</code> 一行完成边界跳转，减少分支嵌套：</p>
<pre><code>class Solution:
    def lengthOfLongestSubstring(self, s: str) -&gt; int:
        last = {}
        ans = j = 0
        for i, c in enumerate(s):
            j = max(j, last.get(c, -1) + 1)
            last[c] = i
            ans = max(ans, i - j + 1)
        return ans
</code></pre>
<p><code>last.get(c, -1) + 1</code> 在 <code>c</code> 首次出现时等于 <code>0</code>，与 <code>j</code> 取 <code>max</code> 后仍保持 <code>j</code> 不变，等价于原来的 <code>if c in last and last[c] &gt;= j</code> 判断。</p>
]]></content>
    <author><name>Hygen</name></author>
    <category term="hot100"/>
  </entry>
  <entry>
    <title>找到字符串中所有字母异位词</title>
    <link href="https://www.hygen.red/posts/hot100/03-%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/009%E6%89%BE%E5%88%B0%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%B8%AD%E6%89%80%E6%9C%89%E5%AD%97%E6%AF%8D%E5%BC%82%E4%BD%8D%E8%AF%8D/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/hot100/03-%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/009%E6%89%BE%E5%88%B0%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%B8%AD%E6%89%80%E6%9C%89%E5%AD%97%E6%AF%8D%E5%BC%82%E4%BD%8D%E8%AF%8D/</id>
    <published>2026-06-28T00:00:00.000Z</published>
    <updated>2026-06-28T00:00:00.000Z</updated>
    <summary>Leetcode-438</summary>
    <content type="html"><![CDATA[<h1>题目描述</h1>
<p><a href="https://leetcode.cn/problems/find-all-anagrams-in-a-string/description/?envType=study-plan-v2&amp;envId=top-100-liked">题目链接</a></p>
<p>给定两个字符串 <code>s</code> 和 <code>p</code>，找到 <code>s</code> 中所有 <code>p</code> 的<strong>字母异位词</strong>的子串，返回这些子串的起始索引。不考虑答案输出的顺序。</p>
<p><strong>字母异位词</strong>指字母相同，但排列不同的字符串。</p>
<h2>输入</h2>
<p>字符串 <code>s</code> 和 <code>p</code></p>
<h2>输出</h2>
<p>所有异位词子串在 <code>s</code> 中的起始下标列表</p>
<h2>约束条件</h2>
<ol>
<li><code>1 &lt;= s.length, p.length &lt;= 3 * 10^4</code></li>
<li><code>s</code> 和 <code>p</code> 仅包含小写英文字母</li>
</ol>
<h1>进阶约束</h1>
<ol>
<li>能否将时间复杂度优化到 $O(n)$？</li>
</ol>
<h2>示例1</h2>
<pre><code>input: s = "cbaebabacd", p = "abc"
output: [0, 6]
</code></pre>
<p>解释：起始下标 <code>0</code> 的子串是 <code>"cba"</code>，下标 <code>6</code> 的子串是 <code>"bac"</code>，二者都是 <code>"abc"</code> 的字母异位词。</p>
<h2>示例2</h2>
<pre><code>input: s = "abab", p = "ab"
output: [0, 1, 2]
</code></pre>
<p>解释：起始下标 <code>0</code> 的子串是 <code>"ab"</code>，下标 <code>1</code> 的子串是 <code>"ba"</code>，下标 <code>2</code> 的子串是 <code>"ab"</code>。</p>
<h1>解题思路</h1>
<p>题目要求找 <code>s</code> 中所有<strong>连续</strong>子串，使其与 <code>p</code> 互为字母异位词——即长度相同、各字符出现次数完全一致。暴力枚举每个起点再排序或逐字符比对，在 $n$ 可达 $3 \times 10^4$ 时会超时。能否在向右扩展子串的同时，<strong>动态维护</strong>「当前窗口的字符频次是否合法」？</p>
<h2>从暴力枚举出发</h2>
<p>最直观的做法：枚举每个起点 <code>i</code>，取长度为 <code>len(p)</code> 的子串，判断它是否与 <code>p</code> 互为异位词。判断方式可以是排序后比较，或用 <code>Counter</code> 比较频次：</p>
<pre><code>from collections import Counter

class Solution:
    def findAnagrams(self, s: str, p: str) -&gt; List[int]:
        res = []
        m = len(p)
        cp = Counter(p)
        for i in range(len(s) - m + 1):
            if Counter(s[i:i + m]) == cp:
                res.append(i)
        return res
</code></pre>
<p>每个起点都要构造一次子串频次，时间复杂度 $O(n \cdot m)$，数据规模下会超时。瓶颈在于<strong>相邻起点的大量重复计算</strong>——窗口右移一位时，只是去掉最左字符、加入最右字符，频次可以增量更新。能否让左右边界只各移动一次？</p>
<h2>滑动窗口：固定长度 + 频次比较</h2>
<p>用 <code>[j, i]</code> 维护一个窗口。右指针 <code>i</code> 每步将 <code>s[i]</code> 的计数 <code>+1</code>；当窗口长度超过 <code>len(p)</code> 时，将 <code>s[j]</code> 的计数 <code>-1</code> 并 <code>j += 1</code>，保持窗口长度恰好为 <code>len(p)</code>。每次窗口就位后，比较窗口频次与 <code>p</code> 的频次是否相等：</p>
<pre><code>from collections import Counter, defaultdict

class Solution:
    def findAnagrams(self, s: str, p: str) -&gt; List[int]:
        m = len(p)
        cp = Counter(p)
        cs = defaultdict(int)
        res = []
        j = 0
        for i, c in enumerate(s):
            cs[c] += 1
            if i - j + 1 &gt; m:
                cs[s[j]] -= 1
                j += 1
            if i - j + 1 == m and cs == cp:
                res.append(j)
        return res
</code></pre>
<p>每次比较两个字典是 $O(|\Sigma|)$，$|\Sigma| = 26$ 视为常数，总时间 $O(n)$。但每次都要做完整字典相等判断，常数偏大——能否<strong>只在不合法时收缩</strong>，而不是每步都全量比较？</p>
<h2>滑动窗口 + 计数：超频则左缩</h2>
<p>思路与「无重复字符的最长子串」的计数写法一脉相承：右指针 <code>i</code> 每步将 <code>s[i]</code> 计数 <code>+1</code>；若某字符在窗口内的出现次数<strong>超过了</strong> <code>p</code> 中该字符的次数（<code>cs[c] &gt; cp[c]</code>），说明当前窗口不可能成为异位词，用 <code>while</code> 从左侧逐个减计数并右移 <code>j</code>，直到该字符不再超频。收缩完毕后，若窗口长度恰好等于 <code>len(p)</code>，则各字符频次必然与 <code>p</code> 完全一致（总数相同且无人超频），记录起点 <code>j</code>：</p>
<pre><code>from collections import Counter, defaultdict

class Solution:
    def findAnagrams(self, s: str, p: str) -&gt; List[int]:
        j = 0
        cs = defaultdict(int)
        cp = Counter(p)
        res = []

        for i, c in enumerate(s):
            cs[c] += 1
            while cs[c] &gt; cp[c]:
                cs[s[j]] -= 1
                j += 1
            if i - j + 1 == len(p):
                res.append(j)
        
        return res
</code></pre>
<p>以 <code>s = "cbaebabacd"</code>、<code>p = "abc"</code> 为例：</p>
<table>
<thead>
<tr>
<th><code>i</code></th>
<th><code>c</code></th>
<th>操作</th>
<th>窗口 <code>[j, i]</code></th>
<th>是否记录</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td><code>c</code></td>
<td>加入 <code>c</code></td>
<td><code>[0,0]</code> 长度 1</td>
<td>—</td>
</tr>
<tr>
<td>1</td>
<td><code>b</code></td>
<td>加入 <code>b</code></td>
<td><code>[0,1]</code> 长度 2</td>
<td>—</td>
</tr>
<tr>
<td>2</td>
<td><code>a</code></td>
<td>加入 <code>a</code></td>
<td><code>[0,2]</code> = <code>"cba"</code> 长度 3</td>
<td>记录 <code>0</code></td>
</tr>
<tr>
<td>3</td>
<td><code>e</code></td>
<td><code>e</code> 不在 <code>p</code> 中，<code>cs['e']=1 &gt; cp['e']=0</code>，左缩至 <code>j=4</code></td>
<td>长度 0</td>
<td>—</td>
</tr>
<tr>
<td>4</td>
<td><code>b</code></td>
<td>加入 <code>b</code></td>
<td><code>[4,4]</code> 长度 1</td>
<td>—</td>
</tr>
<tr>
<td>5</td>
<td><code>a</code></td>
<td>加入 <code>a</code></td>
<td><code>[4,5]</code> = <code>"ba"</code> 长度 2</td>
<td>—</td>
</tr>
<tr>
<td>6</td>
<td><code>b</code></td>
<td><code>cs['b']=2 &gt; cp['b']=1</code>，左缩至 <code>j=5</code></td>
<td><code>[5,6]</code> = <code>"ab"</code> 长度 2</td>
<td>—</td>
</tr>
<tr>
<td>7</td>
<td><code>a</code></td>
<td><code>cs['a']=2 &gt; cp['a']=1</code>，左缩至 <code>j=6</code></td>
<td><code>[6,7]</code> = <code>"ba"</code> 长度 2</td>
<td>—</td>
</tr>
<tr>
<td>8</td>
<td><code>c</code></td>
<td>加入 <code>c</code></td>
<td><code>[6,8]</code> = <code>"bac"</code> 长度 3</td>
<td>记录 <code>6</code></td>
</tr>
</tbody>
</table>
<p>最终答案为 <code>[0, 6]</code>。</p>
<p><code>j</code> 只增不减，每个字符最多入窗、出窗各一次，时间复杂度 $O(n)$。</p>
<h2>复杂度</h2>
<ul>
<li>时间复杂度：$O(n)$，<code>i</code> 与 <code>j</code> 各最多移动 $n$ 次</li>
<li>空间复杂度：$O(|\Sigma|)$，哈希表最多存字符集大小个键；本题仅小写字母，视为 $O(1)$</li>
</ul>
<h1>解题思路（固定窗口 + 匹配计数）</h1>
<p>上一节的「超频则左缩」是<strong>变长窗口</strong>：只有出现非法字符时才收缩，逻辑简洁，且与「至多出现 $k$ 次」类题目共用同一模板。本题还有一个常见写法：<strong>固定窗口长度</strong>始终为 <code>len(p)</code>，用「匹配度」代替字典全量比较。</p>
<p>维护 <code>need = Counter(p)</code> 记录 <code>p</code> 中各字符的目标频次，<code>window</code> 记录当前窗口频次，再用 <code>valid</code> 统计「有多少种字符的窗口计数<strong>恰好等于</strong>目标计数」。右扩时若某字符计数从小于变为等于目标，<code>valid += 1</code>；若从等于变为大于目标，<code>valid -= 1</code>。窗口超长时左缩一位，对离开窗口的字符做对称更新。当 <code>valid == len(need)</code>（所有出现过的字符类型都匹配），当前起点即为异位词：</p>
<pre><code>from collections import Counter, defaultdict

class Solution:
    def findAnagrams(self, s: str, p: str) -&gt; List[int]:
        if len(s) &lt; len(p):
            return []
        need = Counter(p)
        window = defaultdict(int)
        valid = 0
        res = []
        j = 0
        m = len(p)

        for i, c in enumerate(s):
            window[c] += 1
            if c in need:
                if window[c] == need[c]:
                    valid += 1
                elif window[c] == need[c] + 1:
                    valid -= 1

            if i - j + 1 &gt; m:
                left = s[j]
                if left in need:
                    if window[left] == need[left]:
                        valid -= 1
                    elif window[left] == need[left] + 1:
                        valid += 1
                window[left] -= 1
                j += 1

            if valid == len(need):
                res.append(j)

        return res
</code></pre>
<p>两种写法时间均为 $O(n)$，但侧重点不同：</p>
<table>
<thead>
<tr>
<th>对比项</th>
<th>超频则左缩（变长窗口）</th>
<th>匹配计数（固定窗口）</th>
</tr>
</thead>
<tbody>
<tr>
<td>时间</td>
<td>$O(n)$</td>
<td>$O(n)$</td>
</tr>
<tr>
<td>内层循环</td>
<td>有 <code>while</code></td>
<td>无</td>
</tr>
<tr>
<td>合法性判断</td>
<td>任一字符超频即收缩</td>
<td><code>valid</code> 达标即合法</td>
</tr>
<tr>
<td>扩展性</td>
<td>易改为「至多 / 至少 $k$ 次」</td>
<td>适合「频次恰好相等」类题</td>
</tr>
</tbody>
</table>
<p>面试中若强调<strong>通用模板</strong>，推荐匹配计数写法；若刚学完「无重复字符的最长子串」，超频左缩写法更连贯。</p>
<h1>代码优化</h1>
<p>算法层面两种 $O(n)$ 思路都已最优（至少需读一遍 <code>s</code>）。以下优化侧重<strong>常数因子</strong>与<strong>代码简洁度</strong>。</p>
<h2>用长度 26 的数组替代 <code>Counter</code></h2>
<p>题目保证只有小写字母，频次用 <code>ord(c) - ord('a')</code> 映射到下标 <code>0~25</code>，比 <code>Counter</code> / <code>defaultdict</code> 更省开销：</p>
<pre><code>class Solution:
    def findAnagrams(self, s: str, p: str) -&gt; List[int]:
        m = len(p)
        if len(s) &lt; m:
            return []
        cp = [0] * 26
        cs = [0] * 26
        for c in p:
            cp[ord(c) - 97] += 1

        res = []
        j = 0
        for i, c in enumerate(s):
            idx = ord(c) - 97
            cs[idx] += 1
            while cs[idx] &gt; cp[idx]:
                cs[ord(s[j]) - 97] -= 1
                j += 1
            if i - j + 1 == m:
                res.append(j)
        return res
</code></pre>
<p>语义与原写法完全一致，访问数组比哈希表更快，空间也固定为 $O(26)$。</p>
<h2>匹配计数写法：用 <code>diff</code> 数组判断全匹配</h2>
<p>固定窗口写法中，不必维护 <code>valid</code> 变量——用一个 <code>diff[26]</code> 记录「窗口计数与目标计数的差」，窗口就位后检查 <code>diff</code> 是否全零即可：</p>
<pre><code>class Solution:
    def findAnagrams(self, s: str, p: str) -&gt; List[int]:
        m = len(p)
        n = len(s)
        if n &lt; m:
            return []
        diff = [0] * 26
        for c in p:
            diff[ord(c) - 97] += 1

        res = []
        for i, c in enumerate(s):
            diff[ord(c) - 97] -= 1
            if i &gt;= m:
                diff[ord(s[i - m]) - 97] += 1
            if i &gt;= m - 1 and not any(diff):
                res.append(i - m + 1)
        return res
</code></pre>
<p><code>any(diff)</code> 最坏 $O(26)$，仍是 $O(n)$。若追求极致常数，可像 <code>valid</code> 写法一样增量维护「差值为零的字符种类数」，避免每步扫描 26 个位置。</p>
]]></content>
    <author><name>Hygen</name></author>
    <category term="hot100"/>
  </entry>
  <entry>
    <title>和为 K 的子数组</title>
    <link href="https://www.hygen.red/posts/hot100/04-%E5%AD%90%E4%B8%B2/010%E5%92%8C%E4%B8%BAk%E7%9A%84%E5%AD%90%E6%95%B0%E7%BB%84/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/hot100/04-%E5%AD%90%E4%B8%B2/010%E5%92%8C%E4%B8%BAk%E7%9A%84%E5%AD%90%E6%95%B0%E7%BB%84/</id>
    <published>2026-06-28T00:00:00.000Z</published>
    <updated>2026-06-28T00:00:00.000Z</updated>
    <summary>Leetcode-560</summary>
    <content type="html"><![CDATA[<h1>题目描述</h1>
<p><a href="https://leetcode.cn/problems/subarray-sum-equals-k/description/?envType=study-plan-v2&amp;envId=top-100-liked">题目链接</a></p>
<p>给定一个整数数组 <code>nums</code> 和一个整数 <code>k</code>，请你统计并返回该数组中<strong>和为 <code>k</code> 的连续子数组</strong>的个数。</p>
<h2>输入</h2>
<p>整数数组 <code>nums</code>，整数 <code>k</code></p>
<h2>输出</h2>
<p>和为 <code>k</code> 的连续子数组个数</p>
<h2>约束条件</h2>
<ol>
<li><code>1 &lt;= nums.length &lt;= 2 * 10^4</code></li>
<li><code>-1000 &lt;= nums[i] &lt;= 1000</code></li>
<li><code>-10^7 &lt;= k &lt;= 10^7</code></li>
</ol>
<h1>进阶约束</h1>
<ol>
<li>能否将时间复杂度优化到 $O(n)$？</li>
</ol>
<h2>示例1</h2>
<pre><code>input: nums = [1,1,1], k = 2
output: 2
</code></pre>
<p>解释：和为 <code>2</code> 的子数组有 <code>[1,1]</code>（下标 <code>0~1</code>）与 <code>[1,1]</code>（下标 <code>1~2</code>），共 <code>2</code> 个。</p>
<h2>示例2</h2>
<pre><code>input: nums = [1,2,3], k = 3
output: 2
</code></pre>
<p>解释：和为 <code>3</code> 的子数组有 <code>[1,2]</code>（下标 <code>0~1</code>）与 <code>[3]</code>（下标 <code>2</code>），共 <code>2</code> 个。</p>
<h1>解题思路</h1>
<p>题目要求统计<strong>连续</strong>子数组中和恰好等于 <code>k</code> 的个数。暴力枚举所有子区间再逐个求和，在 $n$ 可达 $2 \times 10^4$ 时会超时。能否把「区间求和」转化为某种<strong>可复用的前缀信息</strong>，并在一次扫描中完成统计？</p>
<h2>从暴力枚举出发</h2>
<p>最直观的做法：枚举每个起点 <code>i</code> 和终点 <code>j</code>，计算 <code>nums[i:j+1]</code> 的和，等于 <code>k</code> 则计数：</p>
<pre><code>class Solution:
    def subarraySum(self, nums: List[int], k: int) -&gt; int:
        ans = 0
        n = len(nums)
        for i in range(n):
            s = 0
            for j in range(i, n):
                s += nums[j]
                if s == k:
                    ans += 1
        return ans
</code></pre>
<p>内层循环从 <code>i</code> 向右累加，避免重复计算区间和，时间复杂度 $O(n^2)$，仍会超时。瓶颈在于：对每个终点 <code>j</code>，我们要知道<strong>有多少个起点</strong> <code>i</code>，使得 <code>nums[i:j+1]</code> 的和为 <code>k</code>——能否不逐个枚举 <code>i</code>？</p>
<h2>前缀和：把区间和变成两个前缀的差</h2>
<p>设 <code>prefix[j]</code> 为 <code>nums[0..j]</code> 的前缀和，则子数组 <code>nums[i+1..j]</code> 的和为：</p>
<p>$$
\text{prefix}[j] - \text{prefix}[i] = k \quad \Leftrightarrow \quad \text{prefix}[i] = \text{prefix}[j] - k
$$</p>
<p>对每个终点 <code>j</code>，问题转化为：在 <code>j</code> 之前，有多少个前缀和恰好等于 <code>prefix[j] - k</code>？若用哈希表记录「每个前缀和出现了几次」，查询就是 $O(1)$ 的。</p>
<h2>哈希表统计前缀和频次</h2>
<p>从左到右扫描，维护当前前缀和 <code>Sum</code>，并用字典 <code>d</code> 记录已出现过的前缀和及其频次：</p>
<ol>
<li><strong><code>d[0] = 1</code></strong>：空前缀的和为 <code>0</code>，出现 <code>1</code> 次。这样当某个子数组本身（从下标 <code>0</code> 开始）的和恰好为 <code>k</code> 时，<code>Sum - k = 0</code> 也能被正确计数。</li>
<li><strong>加入 <code>nums[i]</code></strong>：<code>Sum += x</code>，当前前缀和更新。</li>
<li><strong>累加答案</strong>：<code>ans += d[Sum - k]</code>，即统计此前有多少个前缀和等于 <code>Sum - k</code>，每个都对应一段和为 <code>k</code> 的子数组。</li>
<li><strong>更新频次</strong>：<code>d[Sum] += 1</code>，把当前前缀和记入哈希表，供后续位置查询。</li>
</ol>
<p>以 <code>nums = [1, 2, 3]</code>、<code>k = 3</code> 为例：</p>
<table>
<thead>
<tr>
<th><code>i</code></th>
<th><code>x</code></th>
<th><code>Sum</code></th>
<th><code>Sum - k</code></th>
<th><code>d[Sum-k]</code></th>
<th><code>ans</code></th>
<th><code>d</code>（更新后）</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>1</td>
<td>1</td>
<td>-2</td>
<td>0</td>
<td>0</td>
<td><code>{0:1, 1:1}</code></td>
</tr>
<tr>
<td>1</td>
<td>2</td>
<td>3</td>
<td>0</td>
<td>1</td>
<td>1</td>
<td><code>{0:1, 1:1, 3:1}</code></td>
</tr>
<tr>
<td>2</td>
<td>3</td>
<td>6</td>
<td>3</td>
<td>1</td>
<td>2</td>
<td><code>{0:1, 1:1, 3:1, 6:1}</code></td>
</tr>
</tbody>
</table>
<ul>
<li><code>i = 1</code> 时：<code>Sum = 3</code>，<code>d[0] = 1</code> 表示存在空前缀，对应子数组 <code>[1, 2]</code>（下标 <code>0~1</code>）。</li>
<li><code>i = 2</code> 时：<code>Sum = 6</code>，<code>d[3] = 1</code> 表示此前前缀和 <code>3</code> 出现一次（在 <code>i = 1</code>），对应子数组 <code>[3]</code>（下标 <code>2</code>）。</li>
</ul>
<p>最终答案为 <code>2</code>。</p>
<p>以 <code>nums = [1, 1, 1]</code>、<code>k = 2</code> 为例：</p>
<table>
<thead>
<tr>
<th><code>i</code></th>
<th><code>x</code></th>
<th><code>Sum</code></th>
<th><code>Sum - k</code></th>
<th><code>d[Sum-k]</code></th>
<th><code>ans</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>1</td>
<td>1</td>
<td>-1</td>
<td>0</td>
<td>0</td>
</tr>
<tr>
<td>1</td>
<td>1</td>
<td>2</td>
<td>0</td>
<td>1</td>
<td>1</td>
</tr>
<tr>
<td>2</td>
<td>1</td>
<td>3</td>
<td>1</td>
<td>1</td>
<td>2</td>
</tr>
</tbody>
</table>
<p><code>i = 1</code> 时命中空前缀，得到 <code>[1, 1]</code>（下标 <code>0~1</code>）；<code>i = 2</code> 时命中前缀和 <code>1</code>，得到 <code>[1, 1]</code>（下标 <code>1~2</code>）。注意<strong>相同数值的子数组因起点不同要分别计数</strong>，哈希表用「频次」而非「是否出现」正是为此服务。</p>
<h2>为何不用滑动窗口？</h2>
<p>若数组元素<strong>全为正数</strong>，可用滑动窗口：右扩累加和，和超过 <code>k</code> 则左缩，和等于 <code>k</code> 时计数。但本题 <code>nums[i]</code> 可为负数——左缩一位后区间和可能变小，也可能变大，无法保证单调性，滑动窗口会漏解或重复计数。前缀和 + 哈希表不依赖单调性，是此题的通用正解。</p>
<h2>复杂度</h2>
<ul>
<li>时间复杂度：$O(n)$，每个元素入表、查表各一次</li>
<li>空间复杂度：$O(n)$，哈希表最多存 $n$ 个不同的前缀和</li>
</ul>
<h2>代码</h2>
<pre><code>from collections import defaultdict

class Solution:
    def subarraySum(self, nums: List[int], k: int) -&gt; int:
        d = defaultdict(int)
        d[0] = 1
        Sum = ans = 0
        for i, x in enumerate(nums):
            Sum += x
            ans += d[Sum - k]
            d[Sum] += 1
        return ans
</code></pre>
<h1>代码优化</h1>
<p>算法层面前缀和 + 哈希表已达 $O(n)$ 最优（至少需读一遍数组），无法再降时间复杂度。以下优化侧重<strong>代码规范</strong>与<strong>可读性</strong>。</p>
<h2>变量命名与 <code>dict.get</code></h2>
<p><code>Sum</code> 按 PEP 8 宜改为 <code>prefix_sum</code> 或 <code>s</code>；若不想引入 <code>defaultdict</code>，普通字典配合 <code>.get(key, 0)</code> 语义等价，依赖更少：</p>
<pre><code>class Solution:
    def subarraySum(self, nums: List[int], k: int) -&gt; int:
        cnt = {0: 1}
        prefix_sum = ans = 0
        for x in nums:
            prefix_sum += x
            ans += cnt.get(prefix_sum - k, 0)
            cnt[prefix_sum] = cnt.get(prefix_sum, 0) + 1
        return ans
</code></pre>
<h2>用 <code>Counter</code> 表达「频次统计」</h2>
<p>若更习惯「计数」语义，可将 <code>d</code> 换为 <code>Counter</code>，逻辑不变：</p>
<pre><code>from collections import Counter

class Solution:
    def subarraySum(self, nums: List[int], k: int) -&gt; int:
        cnt = Counter({0: 1})
        prefix_sum = ans = 0
        for x in nums:
            prefix_sum += x
            ans += cnt[prefix_sum - k]
            cnt[prefix_sum] += 1
        return ans
</code></pre>
<p><code>Counter</code> 对缺失键默认返回 <code>0</code>，与 <code>defaultdict(int)</code> 行为一致。三种写法时间、空间均为 $O(n)$，按团队风格择一即可。</p>
]]></content>
    <author><name>Hygen</name></author>
    <category term="hot100"/>
  </entry>
  <entry>
    <title>滑动窗口最大值</title>
    <link href="https://www.hygen.red/posts/hot100/04-%E5%AD%90%E4%B8%B2/011%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E6%9C%80%E5%A4%A7%E5%80%BC/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/hot100/04-%E5%AD%90%E4%B8%B2/011%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E6%9C%80%E5%A4%A7%E5%80%BC/</id>
    <published>2026-06-28T00:00:00.000Z</published>
    <updated>2026-06-28T00:00:00.000Z</updated>
    <summary>Leetcode-239</summary>
    <content type="html"><![CDATA[<h1>题目描述</h1>
<p><a href="https://leetcode.cn/problems/sliding-window-maximum/description/?envType=study-plan-v2&amp;envId=top-100-liked">题目链接</a></p>
<p>给你一个整数数组 <code>nums</code>，有一个大小为 <code>k</code> 的滑动窗口从数组的最左侧移动到最右侧。你只可以看到在滑动窗口内的 <code>k</code> 个数字，滑动窗口每次只向右移动一位。</p>
<p>返回<strong>滑动窗口中的最大值</strong>组成的数组。</p>
<h2>输入</h2>
<p>整数数组 <code>nums</code>，整数 <code>k</code></p>
<h2>输出</h2>
<p>长度为 <code>n - k + 1</code> 的整数数组，每个元素为对应窗口内的最大值</p>
<h2>约束条件</h2>
<ol>
<li><code>1 &lt;= nums.length &lt;= 10^5</code></li>
<li><code>-10^4 &lt;= nums[i] &lt;= 10^4</code></li>
<li><code>1 &lt;= k &lt;= nums.length</code></li>
</ol>
<h1>进阶约束</h1>
<ol>
<li>能否将时间复杂度优化到 $O(n)$？</li>
</ol>
<h2>示例1</h2>
<pre><code>input: nums = [1,3,-1,-3,5,3,6,7], k = 3
output: [3,3,5,5,6,7]
</code></pre>
<p>解释：窗口位置与最大值如下：</p>
<table>
<thead>
<tr>
<th>窗口位置</th>
<th>窗口内元素</th>
<th>最大值</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>[1  3  -1]</code></td>
<td><code>1, 3, -1</code></td>
<td><code>3</code></td>
</tr>
<tr>
<td><code>[3  -1  -3]</code></td>
<td><code>3, -1, -3</code></td>
<td><code>3</code></td>
</tr>
<tr>
<td><code>[-1  -3  5]</code></td>
<td><code>-1, -3, 5</code></td>
<td><code>5</code></td>
</tr>
<tr>
<td><code>[-3  5  3]</code></td>
<td><code>-3, 5, 3</code></td>
<td><code>5</code></td>
</tr>
<tr>
<td><code>[5  3  6]</code></td>
<td><code>5, 3, 6</code></td>
<td><code>6</code></td>
</tr>
<tr>
<td><code>[3  6  7]</code></td>
<td><code>3, 6, 7</code></td>
<td><code>7</code></td>
</tr>
</tbody>
</table>
<h2>示例2</h2>
<pre><code>input: nums = [1], k = 1
output: [1]
</code></pre>
<h1>解题思路</h1>
<p>题目要求对每个长度为 <code>k</code> 的连续子数组求最大值。暴力枚举每个窗口再逐个比较，在 $n$ 可达 $10^5$、$k$ 也可能很大时会超时。能否在窗口右移时<strong>复用</strong>已算过的信息，而不是每次都重新扫一遍？</p>
<h2>从暴力枚举出发</h2>
<p>最直观的做法：枚举每个窗口起点 <code>i</code>，在 <code>nums[i:i+k]</code> 内找最大值：</p>
<pre><code>class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -&gt; List[int]:
        res = []
        n = len(nums)
        for i in range(n - k + 1):
            res.append(max(nums[i:i + k]))
        return res
</code></pre>
<p>每个窗口调用 <code>max</code> 扫 $k$ 个元素，时间复杂度 $O(nk)$。当 $k$ 接近 $n$ 时接近 $O(n^2)$，会超时。瓶颈在于：<strong>相邻两个窗口有大量重叠元素</strong>，却每次都从零开始比较——能否记住「谁还有资格当最大值」？</p>
<h2>堆：动态维护窗口内的最大值</h2>
<p>用<strong>大根堆</strong>存窗口内的 <code>(值, 下标)</code>，每次右移：</p>
<ol>
<li>把新元素 <code>(nums[i], i)</code> 入堆</li>
<li>堆顶若已滑出窗口左边界（<code>下标 &lt;= i - k</code>），则弹出</li>
<li>堆顶即为当前窗口最大值</li>
</ol>
<pre><code>import heapq

class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -&gt; List[int]:
        heap = []
        res = []
        for i, x in enumerate(nums):
            heapq.heappush(heap, (-x, i))
            if i &gt;= k - 1:
                while heap[0][1] &lt;= i - k:
                    heapq.heappop(heap)
                res.append(-heap[0][0])
        return res
</code></pre>
<p>堆中最多 $k$ 个元素，每次入堆、出堆 $O(\log k)$，总时间 $O(n \log k)$，比暴力优，但每个元素仍可能被多次入堆（旧的大值暂时留在堆里需惰性删除），常数也不小。能否让每个下标<strong>最多进出数据结构一次</strong>？</p>
<h2>单调队列：只保留「还有希望成为最大值」的下标</h2>
<p>维护一个队列 <code>q</code>，存的是<strong>下标</strong>（不是值本身），并保证对应数值<strong>从队首到队尾单调递减</strong>——队首 <code>q[0]</code> 永远是当前窗口最大值的下标。</p>
<p>对每个新元素 <code>nums[i]</code>：</p>
<ol>
<li><strong>从队尾淘汰无用下标</strong>：若 <code>nums[q[-1]] &lt;= nums[i]</code>，则 <code>q[-1]</code> 对应的元素比 <code>i</code> 更旧且更小，在之后任何包含 <code>i</code> 的窗口里都不可能成为最大值，持续 <code>pop</code> 直到队尾更大或队空。</li>
<li><strong>入队</strong>：将 <code>i</code> 追加到队尾。</li>
<li><strong>淘汰滑出窗口的队首</strong>：若 <code>i - q[0] &gt;= k</code>，说明 <code>q[0]</code> 已不在窗口 <code>[i-k+1, i]</code> 内，<code>pop</code> 队首。</li>
<li><strong>记录答案</strong>：当 <code>i &gt;= k - 1</code> 时，窗口成形，答案为 <code>nums[q[0]]</code>。</li>
</ol>
<p>以 <code>nums = [1, 3, -1, -3, 5, 3, 6, 7]</code>、<code>k = 3</code> 为例：</p>
<table>
<thead>
<tr>
<th><code>i</code></th>
<th><code>x</code></th>
<th>队尾淘汰</th>
<th><code>q</code>（下标）</th>
<th>对应值</th>
<th>滑出淘汰</th>
<th><code>res</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>1</td>
<td>—</td>
<td><code>[0]</code></td>
<td><code>[1]</code></td>
<td>—</td>
<td>—</td>
</tr>
<tr>
<td>1</td>
<td>3</td>
<td>弹出 <code>0</code>（<code>1≤3</code>）</td>
<td><code>[1]</code></td>
<td><code>[3]</code></td>
<td>—</td>
<td>—</td>
</tr>
<tr>
<td>2</td>
<td>-1</td>
<td>—</td>
<td><code>[1,2]</code></td>
<td><code>[3,-1]</code></td>
<td>—</td>
<td><code>[3]</code></td>
</tr>
<tr>
<td>3</td>
<td>-3</td>
<td>—</td>
<td><code>[1,2,3]</code></td>
<td><code>[3,-1,-3]</code></td>
<td>—</td>
<td><code>[3,3]</code></td>
</tr>
<tr>
<td>4</td>
<td>5</td>
<td>弹出 <code>3,2,1</code></td>
<td><code>[4]</code></td>
<td><code>[5]</code></td>
<td>—</td>
<td><code>[3,3,5]</code></td>
</tr>
<tr>
<td>5</td>
<td>3</td>
<td>—</td>
<td><code>[4,5]</code></td>
<td><code>[5,3]</code></td>
<td>—</td>
<td><code>[3,3,5,5]</code></td>
</tr>
<tr>
<td>6</td>
<td>6</td>
<td>弹出 <code>5,4</code></td>
<td><code>[6]</code></td>
<td><code>[6]</code></td>
<td>—</td>
<td><code>[3,3,5,5,6]</code></td>
</tr>
<tr>
<td>7</td>
<td>7</td>
<td>弹出 <code>6</code></td>
<td><code>[7]</code></td>
<td><code>[7]</code></td>
<td>—</td>
<td><code>[3,3,5,5,6,7]</code></td>
</tr>
</tbody>
</table>
<p>关键观察：</p>
<ul>
<li>每个下标<strong>最多入队一次、出队一次</strong>（队尾淘汰或队首滑出），摊还 $O(n)$。</li>
<li>队首始终是当前窗口最大值，查询为 $O(1)$。</li>
</ul>
<h2>复杂度</h2>
<ul>
<li>时间复杂度：$O(n)$，每个下标最多进出队列各一次</li>
<li>空间复杂度：$O(k)$，队列最多存 $k$ 个下标</li>
</ul>
<h2>代码</h2>
<pre><code>class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -&gt; List[int]:
        q = []
        res = []
        for i, x in enumerate(nums):
            while q and nums[q[-1]] &lt;= x: q.pop()
            q.append(i)
            if i - q[0] &gt;= k: q.pop(0)
            if i &gt;= k - 1: res.append(nums[q[0]])
        return res
</code></pre>
<h1>解题思路（堆 vs 单调队列）</h1>
<p>上面已给出两种非暴力思路，对比如下：</p>
<table>
<thead>
<tr>
<th>对比项</th>
<th>大根堆 + 惰性删除</th>
<th>单调递减队列</th>
</tr>
</thead>
<tbody>
<tr>
<td>时间</td>
<td>$O(n \log k)$</td>
<td>$O(n)$</td>
</tr>
<tr>
<td>每个下标进出次数</td>
<td>可能多次入堆</td>
<td>最多各一次</td>
</tr>
<tr>
<td>思路难度</td>
<td>较低，堆是常见工具</td>
<td>需理解「单调性」与「淘汰」</td>
</tr>
<tr>
<td>适用</td>
<td>$k$ 很小时足够快</td>
<td>本题最优解</td>
</tr>
</tbody>
</table>
<p>若面试时一时想不出单调队列，<strong>堆</strong>是可靠的 $O(n \log k)$ 备选；若要求 $O(n)$ 或 $k$ 很大，单调队列是标准正解。两者都利用了「窗口右移时淘汰过期信息」，但单调队列通过<strong>按值单调</strong>直接扔掉永远不可能胜出的候选，避免了堆的重复存储与惰性删除。</p>
<h1>代码优化</h1>
<p>算法层面单调队列已达 $O(n)$ 最优（至少需读一遍数组）。以下优化侧重<strong>实现细节</strong>：列表 <code>pop(0)</code> 会整体前移元素，单次 $O(k)$，最坏总复杂度退化为 $O(nk)$；改用双端队列可使队首删除变为 $O(1)$。</p>
<h2>用 <code>deque</code> 替代列表作队列</h2>
<p><code>collections.deque</code> 的 <code>popleft()</code> 为 $O(1)$，语义与 <code>pop(0)</code> 相同，整体保证 $O(n)$：</p>
<pre><code>from collections import deque

class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -&gt; List[int]:
        q = deque()
        res = []
        for i, x in enumerate(nums):
            while q and nums[q[-1]] &lt;= x:
                q.pop()
            q.append(i)
            if i - q[0] &gt;= k:
                q.popleft()
            if i &gt;= k - 1:
                res.append(nums[q[0]])
        return res
</code></pre>
<h2>合并条件，减少嵌套</h2>
<p>窗口滑出判断可与答案记录写在同一逻辑块，用早期 <code>continue</code> 跳过未成形的窗口，主流程更清晰：</p>
<pre><code>from collections import deque

class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -&gt; List[int]:
        q = deque()
        res = []
        for i, x in enumerate(nums):
            while q and nums[q[-1]] &lt;= x:
                q.pop()
            q.append(i)
            if i &lt; k - 1:
                continue
            if q[0] &lt;= i - k:
                q.popleft()
            res.append(nums[q[0]])
        return res
</code></pre>
<p>注意此处先判断 <code>i &gt;= k - 1</code> 再淘汰队首，与原版「先淘汰再取答案」等价：当 <code>i = k - 1</code> 时，队首下标最小为 <code>0</code>，<code>0 &lt;= i - k</code> 不成立，不会误删。两种写法时间均为 $O(n)$，推荐 <code>deque</code> + 适当换行以兼顾性能与可读性。</p>
]]></content>
    <author><name>Hygen</name></author>
    <category term="hot100"/>
  </entry>
  <entry>
    <title>锯齿形数组的总数Ⅰ</title>
    <link href="https://www.hygen.red/posts/dsa/solution/013%E9%94%AF%E9%BD%BF%E5%BD%A2%E6%95%B0%E7%BB%84%E7%9A%84%E6%80%BB%E6%95%B0%E2%85%B0/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/dsa/solution/013%E9%94%AF%E9%BD%BF%E5%BD%A2%E6%95%B0%E7%BB%84%E7%9A%84%E6%80%BB%E6%95%B0%E2%85%B0/</id>
    <published>2026-06-23T00:00:00.000Z</published>
    <updated>2026-06-23T00:00:00.000Z</updated>
    <summary>LeetCode-3699</summary>
    <content type="html"><![CDATA[<h1>题目描述</h1>
<p><a href="https://leetcode.cn/problems/number-of-zigzag-arrays-i/description/?envType=daily-question&amp;envId=2026-06-23">题目链接</a></p>
<p>给你三个整数 <code>n</code>、<code>l</code> 和 <code>r</code>。</p>
<p>长度为 <code>n</code> 的<strong>锯齿形数组</strong>定义如下：</p>
<ul>
<li>每个元素的取值范围为 <code>[l, r]</code>；</li>
<li>任意<strong>两个</strong>相邻的元素都不相等；</li>
<li>任意<strong>三个</strong>连续的元素不能构成<strong>严格递增</strong>或<strong>严格递减</strong>的序列。</li>
</ul>
<p>返回满足条件的锯齿形数组的总数。由于答案可能很大，请将结果对 $10^9 + 7$ 取余数。</p>
<p><strong>严格递增</strong>：每个元素都严格大于它的前一个元素（如果存在）。</p>
<p><strong>严格递减</strong>：每个元素都严格小于它的前一个元素（如果存在）。</p>
<h2>输入</h2>
<p>三个整数 <code>n</code>、<code>l</code>、<code>r</code></p>
<h2>输出</h2>
<p>满足条件的锯齿形数组总数（对 $10^9 + 7$ 取余）</p>
<h2>约束条件</h2>
<ol>
<li><code>3 &lt;= n &lt;= 2000</code></li>
<li><code>1 &lt;= l &lt; r &lt;= 2000</code></li>
</ol>
<h2>示例1</h2>
<pre><code>input: n = 3, l = 4, r = 5
output: 2
</code></pre>
<p>解释：取值范围 <code>[4, 5]</code> 内，长度为 <code>3</code> 的锯齿形数组只有 <code>[4, 5, 4]</code> 和 <code>[5, 4, 5]</code> 两种。</p>
<h2>示例2</h2>
<pre><code>input: n = 3, l = 1, r = 3
output: 10
</code></pre>
<p>解释：取值范围 <code>[1, 3]</code> 内，长度为 <code>3</code> 的锯齿形数组共有 10 种，例如 <code>[1, 2, 1]</code>、<code>[2, 1, 3]</code>、<code>[3, 2, 3]</code> 等，均满足锯齿形条件。</p>
<h1>解题思路</h1>
<p>题目要求统计长度为 <code>n</code>、元素在 <code>[l, r]</code> 内、且满足「相邻不等」与「不能有三连单调」的数组总数。<code>n</code> 和取值范围均可达 <code>2000</code>，暴力枚举所有数组显然不可行，需要动态规划。</p>
<h2>从直观做法出发</h2>
<p>最容易想到的是：逐位填数，每填一个新元素时检查「是否与前一数不同」以及「最近三个数是否单调」。这是标准的 DFS / 回溯思路，但状态空间约为 $O(k^n)$（$k = r - l + 1$），在 $n = 2000$ 时完全无法接受。</p>
<p>换一个角度：锯齿形的核心约束其实落在<strong>相邻两数的大小关系</strong>上——</p>
<ul>
<li>「相邻不等」要求 $a_i \neq a_{i+1}$；</li>
<li>「不能有三连单调」等价于：不能连续两步同向变化，即不能出现 $a_{i-1} &lt; a_i &lt; a_{i+1}$（三连增）或 $a_{i-1} &gt; a_i &gt; a_{i+1}$（三连减）。</li>
</ul>
<p>因此，只要知道<strong>最后两个数的大小关系</strong>（递增还是递减），就能判断下一个数该往哪边放，而无需记录更长的历史。</p>
<h2>定义 DP 状态</h2>
<p>将取值 <code>[l, r]</code> 映射为相对下标 <code>0, 1, …, k-1</code>（$k = r - l + 1$），定义：</p>
<ul>
<li><code>f0[i][j]</code>：长度为 <code>i</code>、第 <code>i</code> 个数为 <code>l + j</code>，且<strong>最后两个数严格递增</strong>的方案数；</li>
<li><code>f1[i][j]</code>：长度为 <code>i</code>、第 <code>i</code> 个数为 <code>l + j</code>，且<strong>最后两个数严格递减</strong>的方案数。</li>
</ul>
<p>长度为 <code>1</code> 的数组还没有「最后两个数」，初始化时每个取值各 <code>1</code> 种方案。</p>
<h2>推导转移方程</h2>
<p>要从长度 <code>i - 1</code> 扩展到长度 <code>i</code>，在位置 <code>j</code> 填上新元素 <code>l + j</code>：</p>
<p><strong>转移到 <code>f0[i][j]</code>（末尾递增，即 $a_{i-1} &lt; a_i$）</strong></p>
<p>上一个状态必须是「末尾递减」的 <code>f1[i-1][k]</code>，且要求 $l + k &lt; l + j$，即 $k &lt; j$：</p>
<p>$$f0[i][j] = \sum_{k=0}^{j-1} f1[i-1][k]$$</p>
<p><strong>转移到 <code>f1[i][j]</code>（末尾递减，即 $a_{i-1} &gt; a_i$）</strong></p>
<p>上一个状态必须是「末尾递增」的 <code>f0[i-1][k]</code>，且要求 $l + k &gt; l + j$，即 $k &gt; j$：</p>
<p>$$f1[i][j] = \sum_{k=j+1}^{k-1} f0[i-1][k]$$</p>
<p>直接双重循环转移是 $O(k^2)$ 每层，总复杂度 $O(n \cdot k^2)$，在 $n, k \le 2000$ 时会超时。观察转移式：一个是<strong>前缀和</strong>，一个是<strong>后缀和</strong>。</p>
<h2>前缀和 / 后缀和加速</h2>
<p>对 <code>f1[i-1]</code> 做前缀和 <code>pre</code>，则 $f0[i][j] = pre[j-1]$（$j = 0$ 时为 <code>0</code>）。</p>
<p>对 <code>f0[i-1]</code> 做后缀和 <code>suf</code>，则 $f1[i][j] = suf[j+1]$（$j = k-1$ 时为 <code>0</code>）。</p>
<p>每层更新降为 $O(k)$，总时间 $O(n \cdot k)$。</p>
<h2>滚动数组：一层交替更新</h2>
<p><code>f0</code> 和 <code>f1</code> 交替依赖，可以用<strong>同一个数组</strong>原地滚动：</p>
<ul>
<li>偶数轮（<code>u % 2 == 0</code>）：当前数组视为 <code>f0</code>，做<strong>后缀和</strong>得到新的 <code>f1</code>；</li>
<li>奇数轮（<code>u % 2 == 1</code>）：当前数组视为 <code>f1</code>，做<strong>前缀和</strong>得到新的 <code>f0</code>。</li>
</ul>
<p>以 <code>n = 3, l = 4, r = 5</code>（$k = 2$）为例：</p>
<table>
<thead>
<tr>
<th>轮次 <code>u</code></th>
<th>操作</th>
<th>数组 <code>f</code></th>
<th>含义</th>
</tr>
</thead>
<tbody>
<tr>
<td>初始</td>
<td>—</td>
<td><code>[1, 1]</code></td>
<td>长度 1，每个取值 1 种</td>
</tr>
<tr>
<td>0</td>
<td>后缀和</td>
<td><code>[1, 0]</code></td>
<td>长度 2，末尾递减</td>
</tr>
<tr>
<td>1</td>
<td>前缀和</td>
<td><code>[0, 1]</code></td>
<td>长度 3，末尾递增</td>
</tr>
</tbody>
</table>
<p><code>sum(f) = 1</code>，最终答案 <code>1 × 2 = 2</code>，与示例一致。</p>
<h2>为何最后乘以 2</h2>
<p>经过 <code>n - 1</code> 轮转移后，数组里只保留了<strong>一种末尾方向</strong>的状态（<code>n</code> 为奇数时对应 <code>f0</code>，<code>n</code> 为偶数时对应 <code>f1</code>）。</p>
<p>但合法的锯齿形数组在结尾处可以是「末尾递增」或「末尾递减」两种，且由于前缀和与后缀和的对称性，两种方向的方案总数相等：</p>
<p>$$\sum_j f0[n][j] = \sum_j f1[n][j]$$</p>
<p>因此最终答案为 <code>sum(f) * 2</code>。</p>
<h2>复杂度</h2>
<ul>
<li>时间复杂度：$O(n \cdot k)$，其中 $k = r - l + 1$</li>
<li>空间复杂度：$O(k)$，只维护一个滚动数组</li>
</ul>
<h2>代码</h2>
<pre><code>class Solution:
    def zigZagArrays(self, n: int, l: int, r: int) -&gt; int:
        """
        f0[i][j]: 包含 i 个数，第 i 个数是 j，且后两个数递增的方案数
        f1[i][j]: 包含 i 个数，第 i 个数是 j，且后两个数递减的方案数

        f0[i][j] = sum(f1[i - 1][k], k in [0, j - 1])
        f1[i][j] = sum(f0[i - 1][k], k in [j + 1, k - 1])
        """
        mod = 1_000_000_007
        k = r - l + 1
        f = [1] * k

        for u in range(n - 1):
            if u % 2:
                pre = 0
                for i, v in enumerate(f):
                    f[i] = pre % mod
                    pre += v
            else:
                suf = 0
                for i in range(k - 1, -1, -1):
                    v = f[i]
                    f[i] = suf % mod
                    suf += v

        return sum(f) * 2 % mod
</code></pre>
<h1>解题思路（记录延伸方向）</h1>
<p>上一节用「末尾递增 / 末尾递减」描述状态，与力扣官方提示的 <code>dp[i][dir][x]</code> 本质相同，只是 <code>dir</code> 的编码方式不同。若从<strong>下一步该往哪边走</strong>来理解，转移会更直观。</p>
<h2>状态的另一种含义</h2>
<p>定义 <code>dp[i][dir][x]</code>：长度为 <code>i</code>、以值 <code>x</code> 结尾、且<strong>下一个元素相对 <code>x</code> 必须</strong>：</p>
<ul>
<li><code>dir = 1</code>（上升）：下一个数 $&gt; x$；</li>
<li><code>dir = 0</code>（下降）：下一个数 $&lt; x$。</li>
</ul>
<p>这与上一节的对应关系为：</p>
<table>
<thead>
<tr>
<th>上一节</th>
<th>本节</th>
<th>含义</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>f0[i][j]</code> 末尾递增</td>
<td><code>dp[i][0][j]</code></td>
<td>刚经历了上升，下一步必须下降</td>
</tr>
<tr>
<td><code>f1[i][j]</code> 末尾递减</td>
<td><code>dp[i][1][j]</code></td>
<td>刚经历了下降，下一步必须上升</td>
</tr>
</tbody>
</table>
<p>「不能三连单调」正是强制<strong>方向交替</strong>：每次填数后，下一步的方向与上一步相反。</p>
<h2>转移（与官方提示一致）</h2>
<p>若当前要求<strong>上升</strong>（<code>dir = 1</code>），可从所有「上一步要求下降、且值小于 <code>y</code>」的状态转移而来：</p>
<p>$$dp[i+1][0][y] \mathrel{+}= \sum_{x &lt; y} dp[i][1][x]$$</p>
<p>若当前要求<strong>下降</strong>（<code>dir = 0</code>），则从所有「上一步要求上升、且值大于 <code>y</code>」的状态转移：</p>
<p>$$dp[i+1][1][y] \mathrel{+}= \sum_{x &gt; y} dp[i][0][x]$$</p>
<p>这与上一节的 <code>f0</code> / <code>f1</code> 转移完全等价，同样可用前缀和 / 后缀和优化到 $O(n \cdot k)$。</p>
<h2>何时需要显式维护 <code>dir</code></h2>
<p>当题目要求输出<strong>以特定方向结尾</strong>的方案数，或需要区分「第一个元素比第二个大 / 小」时，应同时维护 <code>dp[i][0]</code> 和 <code>dp[i][1]</code> 两个数组，而不能在结尾用 <code>× 2</code> 合并。</p>
<p>本题只统计总数，且两种结尾方向的方案数对称相等，故压缩为一个滚动数组并在最后 <code>× 2</code> 是合理的。</p>
<h1>代码优化</h1>
<p>上一节的实现已经是时间最优的 $O(n \cdot k)$ 写法。以下从<strong>可读性</strong>和<strong>语义清晰度</strong>两方面给出优化建议。</p>
<h2>显式维护 f0 / f1，避免 ×2 技巧</h2>
<p>将两种末尾方向拆成两个数组，逻辑更直白，也省去结尾 <code>× 2</code> 的对称性说明：</p>
<pre><code>class Solution:
    def zigZagArrays(self, n: int, l: int, r: int) -&gt; int:
        mod = 1_000_000_007
        k = r - l + 1
        f0 = f1 = [1] * k  # 长度 1 时两种方向尚未区分，各自为 1

        for _ in range(n - 1):
            # f0[i][j] = prefix_sum(f1[i-1])[j]
            pre = 0
            new_f0 = [0] * k
            for j in range(k):
                new_f0[j] = pre % mod
                pre = (pre + f1[j]) % mod

            # f1[i][j] = suffix_sum(f0[i-1])[j]
            suf = 0
            new_f1 = [0] * k
            for j in range(k - 1, -1, -1):
                new_f1[j] = suf % mod
                suf = (suf + f0[j]) % mod

            f0, f1 = new_f0, new_f1

        return (sum(f0) + sum(f1)) % mod
</code></pre>
<p>注意：长度 1 时 <code>f0</code> 与 <code>f1</code> 同为 <code>[1,…,1]</code> 会在第一次转移时各自统计到所有合法的长度 2 序列，不会重复计数。渐近复杂度不变，空间为 $O(k)$（两个数组可进一步优化为交替复用）。</p>
<h2>抽取前缀 / 后缀更新为内联循环</h2>
<p>原实现用 <code>u % 2</code> 在两种更新间切换，极为紧凑。若更在意可读性，可将「前缀和原地变换」和「后缀和原地变换」写成两个独立的小循环，避免读者反复推断奇偶轮的含义：</p>
<pre><code>def to_prefix(f: list[int], mod: int) -&gt; None:
    pre = 0
    for i, v in enumerate(f):
        f[i] = pre % mod
        pre = (pre + v) % mod

def to_suffix(f: list[int], mod: int) -&gt; None:
    suf = 0
    for i in range(len(f) - 1, -1, -1):
        v = f[i]
        f[i] = suf % mod
        suf = (suf + v) % mod
</code></pre>
<p>主循环中按轮次调用 <code>to_suffix</code> / <code>to_prefix</code> 即可，行为与原代码完全一致。</p>
<h2>小结</h2>
<table>
<thead>
<tr>
<th>写法</th>
<th>优点</th>
<th>适用场景</th>
</tr>
</thead>
<tbody>
<tr>
<td>单数组 + 奇偶切换 + <code>× 2</code></td>
<td>最省空间、代码最短</td>
<td>竞赛、已理解对称性时</td>
</tr>
<tr>
<td>双数组 <code>f0</code> / <code>f1</code></td>
<td>语义清晰、无需 <code>× 2</code></td>
<td>初学 DP、需要调试中间状态</td>
</tr>
<tr>
<td>抽取前缀 / 后缀函数</td>
<td>可读性好、便于复用</td>
<td>团队代码、题解讲解</td>
</tr>
</tbody>
</table>
<p>本题 $n, k \le 2000$，三种写法的时间均为 $O(n \cdot k)$，空间均为 $O(k)$，性能差距极小。日常刷题推荐原版的单数组写法；若需要向他人讲解，双数组版本更易于逐步验证。</p>
]]></content>
    <author><name>Hygen</name></author>
    <category term="Algorithm and Structure"/>
  </entry>
  <entry>
    <title>气球的最大数量</title>
    <link href="https://www.hygen.red/posts/dsa/solution/012%E6%B0%94%E7%90%83%E7%9A%84%E6%9C%80%E5%A4%A7%E6%95%B0%E9%87%8F/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/dsa/solution/012%E6%B0%94%E7%90%83%E7%9A%84%E6%9C%80%E5%A4%A7%E6%95%B0%E9%87%8F/</id>
    <published>2026-06-22T00:00:00.000Z</published>
    <updated>2026-06-22T00:00:00.000Z</updated>
    <summary>LeetCode-1189</summary>
    <content type="html"><![CDATA[<h1>题目描述</h1>
<p><a href="https://leetcode.cn/problems/maximum-number-of-balloons/description/?envType=daily-question&amp;envId=2026-06-22">题目链接</a></p>
<p>给你一个字符串 <code>text</code>，你可以使用其中的字符来组成尽可能多的单词 <code>"balloon"</code>。</p>
<p>给你一个字符串 <code>text</code>，计算并返回能够拼成 <code>"balloon"</code> 的<strong>最大数量</strong>。</p>
<h2>输入</h2>
<p>字符串 <code>text</code></p>
<h2>输出</h2>
<p>能拼出的 <code>"balloon"</code> 最大数量</p>
<h2>约束条件</h2>
<ol>
<li><code>1 &lt;= text.length &lt;= 10^4</code></li>
<li><code>text</code> 仅由小写英文字母组成</li>
</ol>
<h2>示例1</h2>
<pre><code>input: text = "nlaebolko"
output: 1
</code></pre>
<p>解释：用 <code>text</code> 中的字符可以拼出一个 <code>"balloon"</code>。</p>
<h2>示例2</h2>
<pre><code>input: text = "loonbalxballpoon"
output: 2
</code></pre>
<p>解释：可以拼出两个 <code>"balloon"</code>，剩余字符无法继续拼出第三个。</p>
<h2>示例3</h2>
<pre><code>input: text = "leetcode"
output: 0
</code></pre>
<p>解释：<code>text</code> 中没有字母 <code>b</code>，无法拼出任何 <code>"balloon"</code>。</p>
<h1>解题思路</h1>
<p>题目要求用 <code>text</code> 中的字符拼出尽可能多的 <code>"balloon"</code>。每个字符只能使用一次，且拼出的单词必须恰好是 <code>"balloon"</code>。</p>
<h2>从直观做法出发</h2>
<p>最容易想到的是：在 <code>text</code> 里反复「找齐」一组 <code>b-a-l-l-o-o-n</code>，凑齐就 <code>ans += 1</code>，直到再也凑不齐为止。但每凑一组都要在剩余字符里重新扫描、匹配，实现繁琐，也容易在重复字母上出错。</p>
<p>换一个角度：与其模拟「一组一组地拼」，不如先问——<strong>拼一个 <code>"balloon"</code> 到底需要哪些字母、各要几个？</strong> 答案一旦固定，整道题就变成了「资源够不够」的问题。</p>
<h2>拆解单词 balloon</h2>
<p>把 <code>"balloon"</code> 的字母拆开统计：</p>
<table>
<thead>
<tr>
<th>字母</th>
<th><code>b</code></th>
<th><code>a</code></th>
<th><code>l</code></th>
<th><code>o</code></th>
<th><code>n</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>出现次数</td>
<td>1</td>
<td>1</td>
<td>2</td>
<td>2</td>
<td>1</td>
</tr>
</tbody>
</table>
<p>也就是说，每拼出 <strong>1</strong> 个 <code>"balloon"</code>，就要消耗 <code>1</code> 个 <code>b</code>、<code>1</code> 个 <code>a</code>、<code>2</code> 个 <code>l</code>、<code>2</code> 个 <code>o</code>、<code>1</code> 个 <code>n</code>。</p>
<h2>统计字母频次</h2>
<p>因此第一步很直接：遍历 <code>text</code>，统计每个字母出现了多少次。Python 里用 <code>Counter(text)</code> 一行即可完成。</p>
<p>以 <code>text = "loonbalxballpoon"</code> 为例，统计结果为：</p>
<table>
<thead>
<tr>
<th>字母</th>
<th><code>b</code></th>
<th><code>a</code></th>
<th><code>l</code></th>
<th><code>o</code></th>
<th><code>n</code></th>
<th>其他</th>
</tr>
</thead>
<tbody>
<tr>
<td>频次</td>
<td>2</td>
<td>2</td>
<td>4</td>
<td>4</td>
<td>2</td>
<td><code>x</code>: 1, <code>p</code>: 1</td>
</tr>
</tbody>
</table>
<h2>木桶原理：取最小值</h2>
<p>有了各字母的存量，能拼几个 <code>"balloon"</code> 取决于<strong>最紧缺</strong>的那类字母——就像木桶能装多少水，取决于最短的那块板。</p>
<p>每种字母能「支撑」的 balloon 数量如下：</p>
<ul>
<li><code>b</code>：每个 balloon 用 1 个 → 最多 <code>cnt['b']</code> 个</li>
<li><code>a</code>：每个 balloon 用 1 个 → 最多 <code>cnt['a']</code> 个</li>
<li><code>l</code>：每个 balloon 用 2 个 → 最多 <code>cnt['l'] // 2</code> 个</li>
<li><code>o</code>：每个 balloon 用 2 个 → 最多 <code>cnt['o'] // 2</code> 个</li>
<li><code>n</code>：每个 balloon 用 1 个 → 最多 <code>cnt['n']</code> 个</li>
</ul>
<p>最终答案就是上述五个值中的<strong>最小值</strong>：</p>
<p>$$\text{ans} = \min\big(\text{cnt['b']},\ \text{cnt['a']},\ \text{cnt['l']} // 2,\ \text{cnt['o']} // 2,\ \text{cnt['n']}\big)$$</p>
<p>继续以上面的例子：<code>min(2, 2, 4//2, 4//2, 2) = min(2, 2, 2, 2, 2) = 2</code>，与示例输出一致。</p>
<p><code>Counter</code> 对不存在的键会返回 <code>0</code>（例如 <code>text = "leetcode"</code> 中没有 <code>b</code>，<code>cnt['b']</code> 为 <code>0</code>），因此 <code>min</code> 自然得到 <code>0</code>，无需额外判空。</p>
<h2>复杂度</h2>
<ul>
<li>时间复杂度：$O(n)$，$n$ 为 <code>text</code> 长度，统计字母频次需遍历一次</li>
<li>空间复杂度：$O(1)$，至多统计 26 个小写字母</li>
</ul>
<h2>代码</h2>
<pre><code>from collections import Counter

class Solution:
    def maxNumberOfBalloons(self, text: str) -&gt; int:
        cnt = Counter(text)
        return min(cnt['b'], cnt['a'], cnt['l'] // 2, cnt['o'] // 2, cnt['n'])
</code></pre>
<h1>解题思路（只统计相关字母）</h1>
<p>上一节用 <code>Counter(text)</code> 统计了 <code>text</code> 中<strong>所有</strong>字母的频次。但拼 <code>"balloon"</code> 只会用到 <code>b</code>、<code>a</code>、<code>l</code>、<code>o</code>、<code>n</code> 这五个字母——<code>text</code> 里其余的字符（如示例中的 <code>x</code>、<code>p</code>）对答案没有任何贡献。</p>
<h2>为何可以忽略无关字母</h2>
<p>拼单词时，多出来的字母既不能被「存起来」给下次用，也不会参与任何一次 <code>"balloon"</code> 的组成。因此统计阶段只需关心这五个目标字母，其余字符完全可以跳过。</p>
<p>这样做的好处是：</p>
<ul>
<li><strong>思路上更清晰</strong>：问题被精确归结为「五种资源的配额」；</li>
<li><strong>常数更小</strong>：不必为 26 个字母都维护计数（尤其当 <code>text</code> 很长、字母种类很多时）。</li>
</ul>
<h2>单次遍历，按需累加</h2>
<p>维护一个长度为 5 的计数器（或用字典映射），遍历 <code>text</code> 时，若当前字符属于 <code>{b, a, l, o, n}</code> 才累加，否则直接跳过。遍历结束后，同样用木桶原理取 <code>min</code> 即可。</p>
<p>以 <code>text = "leetcode"</code> 为例，五个相关字母的计数全为 <code>0</code>，<code>min</code> 结果为 <code>0</code>；以 <code>text = "nlaebolko"</code> 为例，统计得 <code>b=1, a=1, l=2, o=2, n=1</code>，<code>min(1, 1, 1, 1, 1) = 1</code>。</p>
<p>逻辑与上一节完全一致，区别仅在于「统计谁」——从「全体字母」收窄为「目标字母」。</p>
<h1>代码优化</h1>
<p><code>Counter</code> 写法已经足够简洁正确。若希望在代码层面进一步精简或避免依赖 <code>collections</code>，可以从以下方向优化。</p>
<h2>用固定需求表 + 循环代替手写 <code>min</code></h2>
<p>把每种字母的需求写进字典，用循环统一计算「该字母最多能支撑几个 balloon」，避免在 <code>return</code> 里重复写五个参数：</p>
<pre><code>class Solution:
    def maxNumberOfBalloons(self, text: str) -&gt; int:
        need = {'b': 1, 'a': 1, 'l': 2, 'o': 2, 'n': 1}
        cnt = {c: 0 for c in need}
        for c in text:
            if c in cnt:
                cnt[c] += 1
        return min(cnt[c] // need[c] for c in need)
</code></pre>
<p>字母种类固定为 5，循环开销可忽略；若日后单词变化，只需改 <code>need</code> 字典，扩展性更好。</p>
<h2>用长度为 26 的数组代替字典</h2>
<p>字母均为小写，<code>ord(c) - ord('a')</code> 可作下标，用整型数组计数，访问比字典略快：</p>
<pre><code>class Solution:
    def maxNumberOfBalloons(self, text: str) -&gt; int:
        cnt = [0] * 26
        for c in text:
            cnt[ord(c) - ord('a')] += 1
        return min(cnt[ord(c) - ord('a')] // need
                     for c, need in [('b', 1), ('a', 1), ('l', 2), ('o', 2), ('n', 1)])
</code></pre>
<p>渐近复杂度仍为 $O(n)$ 时间、$O(1)$ 空间，适合对常数敏感的场景。</p>
<h2>小结</h2>
<table>
<thead>
<tr>
<th>写法</th>
<th>优点</th>
<th>适用场景</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Counter</code> + 手写 <code>min</code></td>
<td>最短、最易读</td>
<td>日常刷题、面试快速作答</td>
</tr>
<tr>
<td>需求表 + 循环</td>
<td>易扩展、逻辑集中</td>
<td>单词组成可能变化时</td>
</tr>
<tr>
<td>数组计数</td>
<td>常数更小</td>
<td>对性能有要求的实现</td>
</tr>
</tbody>
</table>
<p>本题数据规模 $n \le 10^4$，三种写法差距极小；首选 <code>Counter</code> 版本即可，其余作为代码风格与扩展性的备选。</p>
]]></content>
    <author><name>Hygen</name></author>
    <category term="Algorithm and Structure"/>
  </entry>
  <entry>
    <title>雪糕的最大数量</title>
    <link href="https://www.hygen.red/posts/dsa/solution/011%E9%9B%AA%E7%B3%95%E7%9A%84%E6%9C%80%E5%A4%A7%E6%95%B0%E9%87%8F/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/dsa/solution/011%E9%9B%AA%E7%B3%95%E7%9A%84%E6%9C%80%E5%A4%A7%E6%95%B0%E9%87%8F/</id>
    <published>2026-06-21T00:00:00.000Z</published>
    <updated>2026-06-21T00:00:00.000Z</updated>
    <summary>LeetCode-1833</summary>
    <content type="html"><![CDATA[<h1>题目描述</h1>
<p><a href="https://leetcode.cn/problems/maximum-ice-cream-bars/?envType=daily-question&amp;envId=2026-06-21">题目链接</a></p>
<p>夏日炎炎，小男孩 Tony 想买一些雪糕消消暑。</p>
<p>商店中新到 <code>n</code> 支雪糕，用长度为 <code>n</code> 的数组 <code>costs</code> 表示雪糕的定价，其中 <code>costs[i]</code> 表示第 <code>i</code> 支雪糕的现金价格。Tony 一共有 <code>coins</code> 现金可以用于消费，他想要买尽可能多的雪糕。</p>
<p><strong>注意：</strong> Tony 可以按任意顺序购买雪糕。</p>
<p>给你价格数组 <code>costs</code> 和现金量 <code>coins</code>，请你计算并返回 Tony 用 <code>coins</code> 现金能够买到的雪糕的<strong>最大数量</strong>。</p>
<h2>输入</h2>
<p>整数数组 <code>costs</code>，整数 <code>coins</code></p>
<h2>输出</h2>
<p>能买到的雪糕最大数量</p>
<h2>约束条件</h2>
<ol>
<li><code>1 &lt;= costs.length == n &lt;= 10^5</code></li>
<li><code>1 &lt;= costs[i] &lt;= 10^5</code></li>
<li><code>1 &lt;= coins &lt;= 10^8</code></li>
</ol>
<h2>示例1</h2>
<pre><code>input: costs = [1,3,2,4,1], coins = 7
output: 4
</code></pre>
<p>解释：Tony 可以买下标为 0、1、2、4 的雪糕，总价为 <code>1 + 3 + 2 + 1 = 7</code>。</p>
<h2>示例2</h2>
<pre><code>input: costs = [10,6,8,7,7,8], coins = 5
output: 0
</code></pre>
<p>解释：Tony 没有足够的钱买任何一支雪糕。</p>
<h2>示例3</h2>
<pre><code>input: costs = [1,6,3,1,2,5], coins = 20
output: 6
</code></pre>
<p>解释：Tony 可以买下所有的雪糕，总价为 <code>1 + 6 + 3 + 1 + 2 + 5 = 18</code>。</p>
<h1>解题思路</h1>
<p>题目只要求<strong>数量最多</strong>，且 Tony 可以按任意顺序购买。在总花费不超过 <code>coins</code> 的前提下，怎样选才能买到最多支雪糕？</p>
<h2>从直观做法出发</h2>
<p>最容易想到的是：枚举所有子集或排列，找出花费不超过 <code>coins</code> 且数量最大的那一组。但 <code>n</code> 可达 <code>10^5</code>，子集规模是指数级，显然不可行。</p>
<p>换一个角度：每支雪糕的价格不同，而目标只是「多买几支」。在钱有限时，直觉上应该<strong>优先买便宜的</strong>——同样的钱，买便宜的能多换几支。</p>
<h2>贪心：先买最便宜的</h2>
<p>形式化地说：若存在两支雪糕，价格分别为 <code>a &lt; b</code>，而当前方案先买了价格为 <code>b</code> 的那支、却跳过了价格为 <code>a</code> 的，那么把这次购买换成 <code>a</code>，会省下 <code>b - a</code> 的现金，后续至少不会比原来更差（多出来的钱还能继续买别的）。因此，最优方案里一定不会出现「放着更便宜的没买，却买了更贵的」这种情况。</p>
<p>结论：<strong>按价格从低到高依次购买</strong>，能买到的就买，买不到就停——这就是贪心策略。</p>
<h2>排序后线性扫描</h2>
<p>要实现「从便宜到贵」，先把 <code>costs</code> 升序排序，再从左到右遍历：若当前剩余现金 <code>coins</code> 够付这支雪糕的价格 <code>x</code>，就买下来（<code>ans += 1</code>，<code>coins -= x</code>）；否则后面的价格只会更高，直接结束。</p>
<p>以 <code>costs = [1,3,2,4,1], coins = 7</code> 为例，排序后 <code>costs = [1,1,2,3,4]</code>：</p>
<table>
<thead>
<tr>
<th>价格 <code>x</code></th>
<th><code>coins</code>（买前）</th>
<th>能否购买</th>
<th><code>ans</code></th>
<th><code>coins</code>（买后）</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>7</td>
<td>是</td>
<td>1</td>
<td>6</td>
</tr>
<tr>
<td>1</td>
<td>6</td>
<td>是</td>
<td>2</td>
<td>5</td>
</tr>
<tr>
<td>2</td>
<td>5</td>
<td>是</td>
<td>3</td>
<td>3</td>
</tr>
<tr>
<td>3</td>
<td>3</td>
<td>是</td>
<td>4</td>
<td>0</td>
</tr>
<tr>
<td>4</td>
<td>0</td>
<td>否</td>
<td>4</td>
<td>0</td>
</tr>
</tbody>
</table>
<p>最终 <code>ans = 4</code>，与示例输出一致。</p>
<h2>复杂度</h2>
<ul>
<li>时间复杂度：$O(n \log n)$，排序 dominates；扫描为 $O(n)$</li>
<li>空间复杂度：$O(1)$ 或 $O(\log n)$（取决于排序实现是否原地）</li>
</ul>
<h2>代码</h2>
<pre><code>class Solution:
    def maxIceCream(self, costs: List[int], coins: int) -&gt; int:
        costs.sort()
        ans = 0
        for i, x in enumerate(costs):
            if coins &gt;= x:
                ans += 1
                coins -= x
        return ans
</code></pre>
<h1>解题思路（计数排序）</h1>
<p>上一节用内置 <code>sort</code> 完成排序，复杂度 $O(n \log n)$。观察约束：<code>costs[i]</code> 取值范围为 $[1, 10^5]$，是一个<strong>有界整数</strong>——此时可以用<strong>计数排序</strong>在 $O(n + V)$ 时间内完成「按价格分组」，其中 $V$ 为价格上界。</p>
<h2>为何计数排序更合适</h2>
<p>比较排序的下界是 $O(n \log n)$，与元素取值范围无关。当 $V = \max(\text{costs}) \le 10^5$ 且 $n$ 也约为 $10^5$ 时，$O(n + V)$ 与 $O(n \log n)$ 同阶，但常数更小；若 $V \ll n$（价格种类少），计数排序优势更明显。</p>
<p>力扣本题也提示「必须使用计数排序」，核心思想正是：价格范围有限，不必对 $n$ 个元素做通用比较排序。</p>
<h2>按价格桶批量购买</h2>
<p>先统计每个价格 <code>p</code> 出现了多少次 <code>cnt[p]</code>，再从 <code>p = 1</code> 到 <code>max(costs)</code> 枚举：</p>
<ul>
<li>若买光所有价格为 <code>p</code> 的雪糕只需 <code>p * cnt[p]</code>，且 <code>coins</code> 够付，则全部买下；</li>
<li>否则，用剩余现金能买 <code>coins // p</code> 支，买完即停。</li>
</ul>
<p>价格高于 <code>coins</code> 的桶无需再看（买一支都不够），枚举上界可截断为 <code>min(max(costs), coins)</code>。</p>
<p>以 <code>costs = [1,3,2,4,1], coins = 7</code> 为例，计数后 <code>cnt[1]=2, cnt[2]=1, cnt[3]=1, cnt[4]=1</code>：</p>
<table>
<thead>
<tr>
<th>价格 <code>p</code></th>
<th><code>cnt[p]</code></th>
<th>全买花费</th>
<th><code>coins</code>（买前）</th>
<th>操作</th>
<th><code>ans</code></th>
<th><code>coins</code>（买后）</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>2</td>
<td>2</td>
<td>7</td>
<td>全买 2 支</td>
<td>2</td>
<td>5</td>
</tr>
<tr>
<td>2</td>
<td>1</td>
<td>2</td>
<td>5</td>
<td>买 1 支</td>
<td>3</td>
<td>3</td>
</tr>
<tr>
<td>3</td>
<td>1</td>
<td>3</td>
<td>3</td>
<td>买 1 支</td>
<td>4</td>
<td>0</td>
</tr>
<tr>
<td>4</td>
<td>1</td>
<td>4</td>
<td>0</td>
<td>买不起，停止</td>
<td>4</td>
<td>0</td>
</tr>
</tbody>
</table>
<p>答案仍为 <code>4</code>。贪心逻辑与排序扫描完全一致，只是把「逐支遍历」换成了「按价格桶批量处理」。</p>
<h2>复杂度</h2>
<ul>
<li>时间复杂度：$O(n + \min(V, \text{coins}))$，统计 $O(n)$，枚举价格桶 $O(\min(V, \text{coins}))$</li>
<li>空间复杂度：$O(V)$，计数数组</li>
</ul>
<h1>代码优化</h1>
<p>排序贪心已是正确且简洁的写法，优化空间主要在提前终止、写法精简，以及结合计数数组做批量购买。</p>
<h2>买不起时提前 <code>break</code></h2>
<p>排序后价格单调不降，一旦 <code>coins &lt; x</code>，后面的雪糕一定也买不起，无需继续循环：</p>
<pre><code>class Solution:
    def maxIceCream(self, costs: List[int], coins: int) -&gt; int:
        costs.sort()
        ans = 0
        for x in costs:
            if coins &lt; x:
                break
            ans += 1
            coins -= x
        return ans
</code></pre>
<h2>去掉无用的 <code>enumerate</code></h2>
<p>循环中并未使用下标 <code>i</code>，直接 <code>for x in costs</code> 更干净，语义也更贴近「按价格从低到高依次尝试」。</p>
<h2>计数数组 + 批量购买（常数更优）</h2>
<p>当同一价格出现多次时，逐支扣款会产生多余循环。用计数数组按桶处理，每轮用 <code>min(cnt[p], coins // p)</code> 一次算清能买几支：</p>
<pre><code>class Solution:
    def maxIceCream(self, costs: List[int], coins: int) -&gt; int:
        mx = max(costs)
        cnt = [0] * (mx + 1)
        for x in costs:
            cnt[x] += 1

        ans = 0
        for p in range(1, min(mx, coins) + 1):
            if cnt[p] == 0:
                continue
            take = min(cnt[p], coins // p)
            ans += take
            coins -= take * p
            if coins == 0:
                break
        return ans
</code></pre>
<p>与排序版贪心等价，在价格重复较多时循环次数更少；渐近复杂度为 $O(n + \min(V, \text{coins}))$，是本题在思路层面更贴合数据范围的写法。</p>
]]></content>
    <author><name>Hygen</name></author>
    <category term="Algorithm and Structure"/>
  </entry>
  <entry>
    <title>最高建筑高度</title>
    <link href="https://www.hygen.red/posts/dsa/solution/010%E6%9C%80%E9%AB%98%E5%BB%BA%E7%AD%91%E9%AB%98%E5%BA%A6/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/dsa/solution/010%E6%9C%80%E9%AB%98%E5%BB%BA%E7%AD%91%E9%AB%98%E5%BA%A6/</id>
    <published>2026-06-20T00:00:00.000Z</published>
    <updated>2026-06-20T00:00:00.000Z</updated>
    <summary>LeetCode-1840</summary>
    <content type="html"><![CDATA[<h1>题目描述</h1>
<p><a href="https://leetcode.cn/problems/maximum-building-height/?envType=daily-question&amp;envId=2026-06-20">题目链接</a></p>
<p>在一座城市里，你需要建 <code>n</code> 栋新的建筑，编号从 <code>1</code> 到 <code>n</code> 排成一列。城市有如下规定：</p>
<ul>
<li>每栋建筑的高度必须是非负整数；</li>
<li>第 <code>1</code> 栋建筑的高度<strong>必须</strong>是 <code>0</code>；</li>
<li>任意两栋相邻建筑的高度差<strong>不能超过</strong> <code>1</code>。</li>
</ul>
<p>此外，部分建筑还有额外的高度上限。<code>restrictions[i] = [idi, maxHeighti]</code> 表示建筑 <code>idi</code> 的高度不能超过 <code>maxHeighti</code>。题目保证每个 <code>idi</code> 唯一，且建筑 <code>1</code> 不会出现在 <code>restrictions</code> 中。</p>
<p>请你返回所有建筑中<strong>最高</strong>的那栋能达到的<strong>最大高度</strong>。</p>
<h2>输入</h2>
<p>整数 <code>n</code>，二维整数数组 <code>restrictions</code></p>
<h2>输出</h2>
<p>最高建筑能达到的最大高度</p>
<h2>约束条件</h2>
<ol>
<li><code>2 &lt;= n &lt;= 10^9</code></li>
<li><code>0 &lt;= restrictions.length &lt;= min(n - 1, 10^5)</code></li>
<li><code>2 &lt;= idi &lt;= n</code>，<code>idi</code> 互不相同</li>
<li><code>0 &lt;= maxHeighti &lt;= 10^9</code></li>
</ol>
<h2>示例1</h2>
<pre><code>input: n = 5, restrictions = [[2,1],[4,1]]
output: 2
</code></pre>
<p>解释：可使建筑高度为 <code>[0,1,2,1,2]</code>，最高建筑高度为 <code>2</code>。</p>
<h2>示例2</h2>
<pre><code>input: n = 6, restrictions = []
output: 5
</code></pre>
<p>解释：无额外限制时，高度可依次为 <code>[0,1,2,3,4,5]</code>，最高为 <code>5</code>。</p>
<h2>示例3</h2>
<pre><code>input: n = 10, restrictions = [[5,3],[2,5],[7,4],[10,3]]
output: 5
</code></pre>
<p>解释：可使建筑高度为 <code>[0,1,2,3,3,4,4,5,4,3]</code>，最高为 <code>5</code>。</p>
<h1>解题思路</h1>
<p>题目同时施加了三类约束：起点为 <code>0</code>、相邻高度差不超过 <code>1</code>、部分位置有高度上限。<code>n</code> 可达 <code>10^9</code>，无法逐栋枚举，需要找到少量「关键位置」集中处理。</p>
<h2>从直观做法出发</h2>
<p>最容易想到的是：对每一栋建筑 <code>i</code>，从左侧（建筑 <code>1</code> 高度 <code>0</code>）和右侧（建筑 <code>n</code> 无上限）分别推算它能达到的最大高度，再取两者较小值。但关键建筑有 <code>10^5</code> 个、总栋数有 <code>10^9</code> 个，逐栋计算显然不可行。</p>
<p>观察示例可以发现：高度上限只出现在 <code>restrictions</code> 给出的位置，其余建筑的高度由相邻关系和这些「卡点」共同决定。因此真正需要关心的，只有<strong>有限个关键位置</strong>——起点、终点，以及所有受限建筑。</p>
<h2>关键观察：峰值只出现在相邻关键位置之间</h2>
<p>任意合法高度序列中，全局最高点要么落在某个关键位置上，要么落在两个关键位置之间的某栋建筑上。</p>
<p>考虑相邻关键位置 $x_i$ 与 $x_{i+1}$，其间距离为 $d = x_{i+1} - x_i$。若位置 $x_i$ 处高度上界为 $h_i$，$x_{i+1}$ 处为 $h_{i+1}$，则这段区间内能抬起的最高「山峰」高度为：</p>
<p>$$
\text{peak} = \left\lfloor \frac{h_i + h_{i+1} + d}{2} \right\rfloor
$$</p>
<p>直觉上：从左侧以不超过 $h_i$ 的高度出发，以每步最多 $+1$ 的斜率向中间爬升，同时从右侧以不超过 $h_{i+1}$ 的高度以每步最多 $+1$ 向中间爬升，两坡在中点相遇时即得峰值。最终答案就是所有区间 peak 的最大值。</p>
<p>因此问题归结为两步：</p>
<ol>
<li>求出每个关键位置在<strong>全局约束</strong>下的高度上界 $h[i]$；</li>
<li>枚举相邻关键位置对，用上式取最大值。</li>
</ol>
<h2>预处理：补全边界并排序</h2>
<p><code>restrictions</code> 中不含建筑 <code>1</code> 和建筑 <code>n</code>，但它们是天然的关键点。补入边界后统一排序：</p>
<ul>
<li><code>[1, 0]</code>：建筑 <code>1</code> 高度固定为 <code>0</code>；</li>
<li><code>[n, inf]</code>：建筑 <code>n</code> 无高度上限（用 <code>inf</code> 表示）。</li>
</ul>
<pre><code>restrictions += [[1, 0], [n, inf]]
restrictions.sort()
</code></pre>
<p>以示例 1（<code>n = 5, restrictions = [[2,1],[4,1]]</code>）为例，排序后得到：</p>
<table>
<thead>
<tr>
<th>下标 <code>i</code></th>
<th>位置 <code>x</code></th>
<th>限制 <code>limit</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>1</td>
<td>0</td>
</tr>
<tr>
<td>1</td>
<td>2</td>
<td>1</td>
</tr>
<tr>
<td>2</td>
<td>4</td>
<td>1</td>
</tr>
<tr>
<td>3</td>
<td>5</td>
<td>∞</td>
</tr>
</tbody>
</table>
<h2>正向扫描：来自左侧的上界</h2>
<p>从左向右，假设在位置 $x_{i-1}$ 处高度最多为 $h[i-1]$，则走到 $x_i$（距离 $x_i - x_{i-1}$）时，理论上限为 $h[i-1] + (x_i - x_{i-1})$，再与该位置的显式限制取小：</p>
<p>$$
h[i] = \min\bigl(h[i-1] + x_i - x_{i-1},\ \text{limit}_i\bigr)
$$</p>
<p>示例 1 正向扫描：</p>
<table>
<thead>
<tr>
<th><code>i</code></th>
<th><code>x</code></th>
<th>计算</th>
<th><code>h[i]</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>1</td>
<td>起点</td>
<td>0</td>
</tr>
<tr>
<td>1</td>
<td>2</td>
<td><code>min(0+1, 1)</code></td>
<td>1</td>
</tr>
<tr>
<td>2</td>
<td>4</td>
<td><code>min(1+2, 1)</code></td>
<td>1</td>
</tr>
<tr>
<td>3</td>
<td>5</td>
<td><code>min(1+1, ∞)</code></td>
<td>2</td>
</tr>
</tbody>
</table>
<p>此时 <code>h[3]=2</code> 只考虑了从左而来的约束，尚未被右侧限制收紧。</p>
<h2>反向扫描：来自右侧的上界</h2>
<p>再从右向左，用对称逻辑收紧上界。在位置 $x_{i+1}$ 处高度最多为 $h[i+1]$，则位置 $x_i$ 处来自右侧的上界为 $h[i+1] + (x_{i+1} - x_i)$，与当前值取小：</p>
<p>$$
h[i] = \min\bigl(h[i],\ h[i+1] + x_{i+1} - x_i\bigr)
$$</p>
<p>示例 1 反向扫描：</p>
<table>
<thead>
<tr>
<th><code>i</code></th>
<th>计算</th>
<th><code>h[i]</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>2</td>
<td><code>min(1, 2+1)</code></td>
<td>1</td>
</tr>
<tr>
<td>1</td>
<td><code>min(1, 1+2)</code></td>
<td>1</td>
</tr>
<tr>
<td>0</td>
<td><code>min(0, 1+1)</code></td>
<td>0</td>
</tr>
</tbody>
</table>
<p>两轮扫描后，$h[i]$ 即为位置 $x_i$ 在<strong>全部约束</strong>下的高度上界。</p>
<h2>段内取峰值</h2>
<p>最后枚举每对相邻关键位置，套用峰值公式：</p>
<pre><code>max((x[i+1] - x[i] + h[i] + h[i+1]) // 2 for i in range(m - 1))
</code></pre>
<p>示例 1 各段：</p>
<table>
<thead>
<tr>
<th>区间</th>
<th><code>d</code></th>
<th><code>h[i]</code></th>
<th><code>h[i+1]</code></th>
<th>peak</th>
</tr>
</thead>
<tbody>
<tr>
<td>1 → 2</td>
<td>1</td>
<td>0</td>
<td>1</td>
<td><code>(1+0+1)//2 = 1</code></td>
</tr>
<tr>
<td>2 → 4</td>
<td>2</td>
<td>1</td>
<td>1</td>
<td><code>(2+1+1)//2 = 2</code></td>
</tr>
<tr>
<td>4 → 5</td>
<td>1</td>
<td>1</td>
<td>2</td>
<td><code>(1+1+2)//2 = 2</code></td>
</tr>
</tbody>
</table>
<p>最大值为 <code>2</code>，与示例输出一致。</p>
<h2>复杂度</h2>
<ul>
<li>时间复杂度：$O(m \log m)$，$m$ 为关键位置数（<code>restrictions.length + 2</code>），排序 dominates</li>
<li>空间复杂度：$O(m)$，存储高度上界数组</li>
</ul>
<h2>代码</h2>
<pre><code>from math import inf

class Solution:
    def maxBuilding(self, n: int, restrictions: List[List[int]]) -&gt; int:
        restrictions += [[1, 0], [n, inf]]
        restrictions.sort()

        m = len(restrictions)
        h = [0] * m

        for i in range(1, m):
            h[i] = min(h[i - 1] + restrictions[i][0] - restrictions[i - 1][0], restrictions[i][1])
        for i in range(m - 2, -1, -1):
            h[i] = min(h[i], h[i + 1] + restrictions[i + 1][0] - restrictions[i][0])

        return max(restrictions[i + 1][0] - restrictions[i][0] + h[i] + h[i + 1] for i in range(m - 1)) // 2
</code></pre>
<h1>解题思路（斜率视角）</h1>
<p>上一节的正向/反向扫描可以换一个更几何化的角度理解，有助于记住峰值公式从何而来。</p>
<p>相邻建筑高度差不超过 $1$，意味着高度曲线沿编号方向的<strong>斜率绝对值</strong>不超过 $1$——只能以每步 $\pm 1$ 的坡度升降。每个关键位置 $x_i$ 给出一个「天花板」$h[i]$，合法高度曲线必须始终位于这些天花板之下。</p>
<p>在区间 $[x_i, x_{i+1}]$ 上，曲线从左侧天花板 $h[i]$ 出发、以最大斜率 $+1$ 向中间爬升，同时从右侧天花板 $h[i+1]$ 以最大斜率 $+1$（等价于从左看是 $-1$）向中间爬升。两坡在中点相遇时达到最高，该点高度正是：</p>
<p>$$
\left\lfloor \frac{h[i] + h[i+1] + (x_{i+1} - x_i)}{2} \right\rfloor
$$</p>
<p>正向扫描等价于「只保留左坡天花板」，反向扫描等价于「再叠加右坡天花板取交集」。这与力扣相似题 <a href="https://leetcode.cn/problems/find-maximum-value-in-a-constrained-sequence/">找到带限制序列的最大值</a> 的「双向松弛」本质相同，只是本题将松弛后的上界用于段内峰值而非直接输出某一点的值。</p>
<p>从渐近复杂度看，$O(m \log m)$ 已是最优——关键位置数 $m \le 10^5 + 2$，无法再降；也不存在比直接计算峰值更优的算法范式（例如二分答案会引入额外的 $\log H$ 因子，反而更慢）。</p>
<h1>代码优化</h1>
<p>本题时间复杂度已达最优，优化空间主要在边界处理与代码可读性。</p>
<h2>合并位置 <code>n</code> 的重复条目</h2>
<p>若 <code>restrictions</code> 中已包含 <code>[n, maxHeight]</code>，补入 <code>[n, inf]</code> 后排序会出现两个位置为 <code>n</code> 的条目。正确性不受影响（第二趟正向扫描距离为 <code>0</code>，<code>h</code> 不变），但可以在排序后合并：同一位置取 <code>limit</code> 的较小值。</p>
<pre><code>restrictions += [[1, 0], [n, inf]]
restrictions.sort()
merged = [restrictions[0]]
for x, lim in restrictions[1:]:
    if merged[-1][0] == x:
        merged[-1][1] = min(merged[-1][1], lim)
    else:
        merged.append([x, lim])
restrictions = merged
</code></pre>
<h2>拆出位置与限制，减少二维下标</h2>
<p>将 <code>restrictions</code> 拆为 <code>xs</code> 与 <code>limits</code> 两个数组，循环体更紧凑，也避免反复写 <code>restrictions[i][0]</code>：</p>
<pre><code>from math import inf

class Solution:
    def maxBuilding(self, n: int, restrictions: List[List[int]]) -&gt; int:
        restrictions += [[1, 0], [n, inf]]
        restrictions.sort()
        xs = [x for x, _ in restrictions]
        limits = [lim for _, lim in restrictions]
        m = len(xs)
        h = [0] * m

        for i in range(1, m):
            h[i] = min(h[i - 1] + xs[i] - xs[i - 1], limits[i])
        for i in range(m - 2, -1, -1):
            h[i] = min(h[i], h[i + 1] + xs[i + 1] - xs[i])

        return max(xs[i + 1] - xs[i] + h[i] + h[i + 1] for i in range(m - 1)) // 2
</code></pre>
<p>语义与原写法完全一致，仅将「关键位置」与「高度上限」解耦，便于阅读与调试。</p>
]]></content>
    <author><name>Hygen</name></author>
    <category term="Algorithm and Structure"/>
  </entry>
  <entry>
    <title>找到最高海拔</title>
    <link href="https://www.hygen.red/posts/dsa/solution/009%E6%89%BE%E5%88%B0%E6%9C%80%E9%AB%98%E6%B5%B7%E6%8B%94/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/dsa/solution/009%E6%89%BE%E5%88%B0%E6%9C%80%E9%AB%98%E6%B5%B7%E6%8B%94/</id>
    <published>2026-06-19T00:00:00.000Z</published>
    <updated>2026-06-19T00:00:00.000Z</updated>
    <summary>LeetCode-1732</summary>
    <content type="html"><![CDATA[<h1>题目描述</h1>
<p><a href="https://leetcode.cn/problems/find-the-highest-altitude/description/?envType=daily-question&amp;envId=2026-06-19">题目链接</a></p>
<p>有一个自行车手打算进行一场公路骑行，这条路线总共由 <code>n + 1</code> 个不同海拔的点组成。自行车手从海拔为 <code>0</code> 的点 <code>0</code> 开始骑行。</p>
<p>给你一个长度为 <code>n</code> 的整数数组 <code>gain</code>，其中 <code>gain[i]</code> 是点 <code>i</code> 和点 <code>i + 1</code> 的<strong>净海拔增益</strong>（可能为负）。请你返回自行车手在骑行过程中达到的<strong>最高海拔</strong>。</p>
<h2>输入</h2>
<p>整数数组 <code>gain</code></p>
<h2>输出</h2>
<p>骑行过程中达到的最高海拔</p>
<h2>约束条件</h2>
<ol>
<li><code>1 &lt;= gain.length &lt;= 100</code></li>
<li><code>-100 &lt;= gain[i] &lt;= 100</code></li>
</ol>
<h2>示例1</h2>
<pre><code>input: gain = [-5,1,5,0,-7]
output: 1
</code></pre>
<p>解释：海拔依次为 <code>0, -5, -4, 1, 1, -6</code>，最高海拔为 <code>1</code>。</p>
<h2>示例2</h2>
<pre><code>input: gain = [-4,-3,-2,-1,4,3,2]
output: 0
</code></pre>
<p>解释：全程海拔始终不高于起点 <code>0</code>。</p>
<h1>解题思路</h1>
<p>题目给出了相邻两点之间的海拔<strong>变化量</strong>，起点海拔固定为 <code>0</code>。因此，每经过一个路段，当前海拔就是在原有基础上加上对应的 <code>gain[i]</code>——本质上是在做<strong>前缀和</strong>。</p>
<h2>从逐段累加出发</h2>
<p>最自然的想法是：从海拔 <code>0</code> 出发，依次走过每一段路，记录途中出现过的最大海拔。</p>
<p>以 <code>gain = [-5, 1, 5, 0, -7]</code> 为例，逐段累加后各点的海拔为：</p>
<table>
<thead>
<tr>
<th>路段</th>
<th><code>gain[i]</code></th>
<th>当前海拔</th>
</tr>
</thead>
<tbody>
<tr>
<td>起点</td>
<td>—</td>
<td>0</td>
</tr>
<tr>
<td>第 1 段</td>
<td>-5</td>
<td>-5</td>
</tr>
<tr>
<td>第 2 段</td>
<td>1</td>
<td>-4</td>
</tr>
<tr>
<td>第 3 段</td>
<td>5</td>
<td>1</td>
</tr>
<tr>
<td>第 4 段</td>
<td>0</td>
<td>1</td>
</tr>
<tr>
<td>第 5 段</td>
<td>-7</td>
<td>-6</td>
</tr>
</tbody>
</table>
<p>最高海拔出现在第 3 段之后，为 <code>1</code>。</p>
<h2>前缀和：用 <code>accumulate</code> 一次求出各点海拔</h2>
<p>「逐段累加」若把每一步的结果都记下来，就得到了 <code>gain</code> 的<strong>前缀和</strong>序列——第 <code>i</code> 项表示走完前 <code>i</code> 段路后的海拔。</p>
<p>Python 标准库 <code>itertools.accumulate</code> 正好做这件事：它按顺序把 <code>gain</code> 累加，生成每一步的当前海拔。题目要求的是这些海拔（连同起点 <code>0</code>）中的最大值，因此：</p>
<p>$$
\text{ans} = \max\bigl(0,\ \text{accumulate}(\text{gain})\bigr)
$$</p>
<p>外层再与 <code>0</code> 取最大，是因为起点海拔 <code>0</code> 本身也可能就是答案（如示例 2 全程不升反降）。</p>
<h2>复杂度</h2>
<ul>
<li>时间复杂度：$O(n)$，遍历 <code>gain</code> 一次</li>
<li>空间复杂度：$O(1)$，<code>accumulate</code> 返回迭代器，<code>max</code> 边读边比较，不额外存储整个前缀和数组</li>
</ul>
<h2>代码</h2>
<pre><code>from itertools import accumulate

class Solution:
    def largestAltitude(self, gain: List[int]) -&gt; int:
        return max(max(accumulate(gain)), 0)
</code></pre>
<h1>解题思路（单次遍历）</h1>
<p>前缀和视角需要先「算出每一步的海拔」，再取最大。换一个角度：我们其实<strong>不必保存</strong>所有中间海拔，只需在遍历过程中维护两个量——当前海拔 <code>alt</code> 和历史最大 <code>ans</code>。</p>
<p>每读入一个 <code>gain[i]</code>，更新 <code>alt += gain[i]</code>，再用 <code>ans = max(ans, alt)</code> 刷新答案。起点 <code>0</code> 可作为 <code>ans</code> 的初始值，逻辑与上一节完全等价，只是把「先累加、再取 max」合并成了一次扫描。</p>
<p>以 <code>gain = [-5, 1, 5, 0, -7]</code> 为例：</p>
<table>
<thead>
<tr>
<th><code>gain[i]</code></th>
<th><code>alt</code></th>
<th><code>ans</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>—</td>
<td>0</td>
<td>0</td>
</tr>
<tr>
<td>-5</td>
<td>-5</td>
<td>0</td>
</tr>
<tr>
<td>1</td>
<td>-4</td>
<td>0</td>
</tr>
<tr>
<td>5</td>
<td>1</td>
<td>1</td>
</tr>
<tr>
<td>0</td>
<td>1</td>
<td>1</td>
</tr>
<tr>
<td>-7</td>
<td>-6</td>
<td>1</td>
</tr>
</tbody>
</table>
<p>最终 <code>ans = 1</code>。时间、空间复杂度仍为 $O(n)$ 和 $O(1)$，适合在不方便使用 <code>itertools</code> 的环境（如部分 OJ）中手写。</p>
<h1>代码优化</h1>
<p>本题时间复杂度已是 $O(n)$，不存在渐近意义上的更优算法。优化空间主要在表达简洁度与可读性。</p>
<h2>用解包替代嵌套 <code>max</code></h2>
<p><code>max(max(accumulate(gain)), 0)</code> 需要读完整个迭代器才能得出内层最大值，再与 <code>0</code> 比较。利用 <code>max</code> 支持多参数的特性，把起点 <code>0</code> 一并传入更直观：</p>
<pre><code>return max(0, *accumulate(gain))
</code></pre>
<p>语义不变：在起点 <code>0</code> 与所有前缀和之间取最大。</p>
<h2>显式循环（无需 <code>itertools</code>）</h2>
<p>若更偏好「一眼能看懂在干什么」的写法，单次遍历同样紧凑：</p>
<pre><code>class Solution:
    def largestAltitude(self, gain: List[int]) -&gt; int:
        alt = ans = 0
        for g in gain:
            alt += g
            ans = max(ans, alt)
        return ans
</code></pre>
<p>与 <code>accumulate</code> 版本相比，少了标准库依赖，循环体也直接对应「逐段骑行、随时更新最高海拔」的题意——可按场景与个人习惯选择。</p>
]]></content>
    <author><name>Hygen</name></author>
    <category term="Algorithm and Structure"/>
  </entry>
  <entry>
    <title>时钟指针的夹角</title>
    <link href="https://www.hygen.red/posts/dsa/solution/008%E6%97%B6%E9%92%9F%E6%8C%87%E9%92%88%E7%9A%84%E5%A4%B9%E8%A7%92/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/dsa/solution/008%E6%97%B6%E9%92%9F%E6%8C%87%E9%92%88%E7%9A%84%E5%A4%B9%E8%A7%92/</id>
    <published>2026-06-18T00:00:00.000Z</published>
    <updated>2026-06-18T00:00:00.000Z</updated>
    <summary>LeetCode-1344</summary>
    <content type="html"><![CDATA[<h1>题目描述</h1>
<p><a href="https://leetcode.cn/problems/angle-between-hands-of-a-clock/?envType=daily-question&amp;envId=2026-06-18">题目链接</a></p>
<p>给你两个数 <code>hour</code> 和 <code>minutes</code>。请你返回在时钟上，由给定时间的时针和分针组成的<strong>较小角</strong>的角度（60 单位制，即一圈 360°）。</p>
<h2>输入</h2>
<p>整数 <code>hour</code>，整数 <code>minutes</code></p>
<h2>输出</h2>
<p>时针与分针之间的较小夹角（浮点数）</p>
<h2>约束条件</h2>
<ol>
<li><code>1 &lt;= hour &lt;= 12</code></li>
<li><code>0 &lt;= minutes &lt;= 59</code></li>
<li>与标准答案误差在 <code>10^-5</code> 以内的结果都被视为正确结果</li>
</ol>
<h2>示例1</h2>
<pre><code>input: hour = 12, minutes = 30
output: 165
</code></pre>
<h2>示例2</h2>
<pre><code>input: hour = 3, minutes = 30
output: 75
</code></pre>
<h2>示例3</h2>
<pre><code>input: hour = 3, minutes = 15
output: 7.5
</code></pre>
<h2>示例4</h2>
<pre><code>input: hour = 4, minutes = 50
output: 155
</code></pre>
<h2>示例5</h2>
<pre><code>input: hour = 12, minutes = 0
output: 0
</code></pre>
<h1>解题思路</h1>
<p>题目要的是时针与分针之间的<strong>较小夹角</strong>，而不是有向角或较大那个角。核心在于：先把两根指针各自转过的角度算出来，再求它们之间的差，最后取「锐角/钝角中较小的那个」。</p>
<h2>从表盘刻度出发</h2>
<p>标准时钟一圈 360°，均匀分成 12 个大格、60 个小格：</p>
<ul>
<li><strong>分针</strong>：每走 1 分钟转 $360° / 60 = 6°$</li>
<li><strong>时针</strong>：每走 1 小时转 $360° / 12 = 30°$；同时，时针会随分钟缓慢移动——每过 1 分钟再转 $30° / 60 = 0.5°$</li>
</ul>
<p>因此给定 <code>minutes</code> 和 <code>hour</code> 后，两根指针相对于 12 点方向（顺时针）的角度分别为：</p>
<p>$$
m = 6 \times \text{minutes}, \quad h = \text{hour} \times 30 + \frac{\text{minutes}}{2}
$$</p>
<p>以示例 3（<code>hour = 3, minutes = 15</code>）为例：</p>
<table>
<thead>
<tr>
<th>指针</th>
<th>计算</th>
<th>角度</th>
</tr>
</thead>
<tbody>
<tr>
<td>分针</td>
<td>$6 \times 15$</td>
<td>$90°$</td>
</tr>
<tr>
<td>时针</td>
<td>$3 \times 30 + 15/2$</td>
<td>$97.5°$</td>
</tr>
</tbody>
</table>
<p>两根指针几乎重合，夹角应为 $7.5°$。</p>
<h2>求两针夹角</h2>
<p>有了 $h$ 和 $m$，两针的夹角就是它们角度之差的绝对值。但直接写 <code>abs(h - m)</code> 在 $h &lt; m$ 时虽然仍正确，用「补角」形式可以统一处理，避免显式分支：</p>
<p>$$
\text{ans} = (h + 360 - m) \bmod 360
$$</p>
<p>这相当于把 $h - m$ 归一化到 $[0, 360)$：若 $h \ge m$，结果就是 $h - m$；若 $h &lt; m$，则加上 360 再取模，等价于从分针沿顺时针方向走到时针的角度。</p>
<p>仍以示例 3 验证：$\text{ans} = (97.5 + 360 - 90) \bmod 360 = 7.5$。</p>
<h2>取较小角</h2>
<p>表盘上两个方向的角度之和恒为 360°，题目要的是<strong>较小</strong>的那个，因此：</p>
<p>$$
\text{result} = \min(\text{ans},\ 360 - \text{ans})
$$</p>
<p>示例 1（<code>hour = 12, minutes = 30</code>）：$m = 180°$，$h = 12 \times 30 + 15 = 375°$，$\text{ans} = (375 + 360 - 180) \bmod 360 = 195°$，较小角为 $\min(195, 165) = 165°$。</p>
<p>示例 5（<code>hour = 12, minutes = 0</code>）：$m = 0°$，$h = 360°$，$\text{ans} = 0°$，两针重合，返回 $0$。</p>
<h2>复杂度</h2>
<ul>
<li>时间复杂度：$O(1)$，固定几次算术运算</li>
<li>空间复杂度：$O(1)$</li>
</ul>
<h2>代码</h2>
<pre><code>class Solution:
    def angleClock(self, hour: int, minutes: int) -&gt; float:
        m = 6 * minutes
        h = hour * 30 + minutes / 2
        ans = (h + 360 - m) % 360
        return min(ans, 360 - ans)
</code></pre>
<h1>解题思路（相对角速度）</h1>
<p>上面的做法先分别求出两根指针的<strong>绝对角度</strong>，再作差。换一个视角：只关心两针之间的<strong>相对位移</strong>，可以跳过「各自归位到 12 点」这一步。</p>
<p>分针每分钟转 $6°$，时针每分钟转 $0.5°$，相对角速度为 $6 - 0.5 = 5.5°$/分钟。因此，从整点出发，分钟带来的「追赶量」是 $5.5 \times \text{minutes}$；小时刻度带来的初始差距是 $\text{hour} \times 30°$（<code>hour = 12</code> 时等价于 0）。</p>
<p>两针夹角的绝对差可直接写为：</p>
<p>$$
\left| \text{hour} \times 30 - \text{minutes} \times 5.5 \right|
$$</p>
<p>再同样取 $\min(\text{diff},\ 360 - \text{diff})$ 即可。思路与上一节等价，只是把「分针角度」和「时针因分钟产生的偏移」合并成了一项 $5.5 \times \text{minutes}$，心算时更紧凑。</p>
<h1>代码优化</h1>
<p>本题时间复杂度已是 $O(1)$，不存在算法层面的更优渐近复杂度。优化空间主要在代码简洁性与边界表达的清晰度。</p>
<h2>用 <code>hour % 12</code> 处理 12 点</h2>
<p><code>hour = 12</code> 在表盘上与 0 点重合。当前写法中 $h = 12 \times 30 = 360°$，取模后结果仍正确，但语义上 12 点应视为第 0 格。写成 <code>hour % 12</code> 更直观，也避免 $h$ 出现 360、390 这类「绕了一圈」的数值：</p>
<pre><code>h = hour % 12 * 30 + minutes / 2
</code></pre>
<h2>合并为单行表达式</h2>
<p>若更在意代码紧凑，可将相对角速度公式与取较小角合并（需配合 <code>hour % 12</code>）：</p>
<pre><code>class Solution:
    def angleClock(self, hour: int, minutes: int) -&gt; float:
        diff = abs(hour % 12 * 30 - minutes * 5.5)
        return min(diff, 360 - diff)
</code></pre>
<p>与原写法相比，变量更少、一行出结果；代价是 <code>(h + 360 - m) % 360</code> 那种「不用 <code>abs</code>、统一归一化到 $[0, 360)$」的写法在可读性上略胜一筹——可按个人偏好选择。</p>
]]></content>
    <author><name>Hygen</name></author>
    <category term="Algorithm and Structure"/>
  </entry>
  <entry>
    <title>用特殊操作处理字符串Ⅱ</title>
    <link href="https://www.hygen.red/posts/dsa/solution/007%E7%94%A8%E7%89%B9%E6%AE%8A%E6%93%8D%E4%BD%9C%E5%A4%84%E7%90%86%E5%AD%97%E7%AC%A6%E4%B8%B2%E2%85%B1/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/dsa/solution/007%E7%94%A8%E7%89%B9%E6%AE%8A%E6%93%8D%E4%BD%9C%E5%A4%84%E7%90%86%E5%AD%97%E7%AC%A6%E4%B8%B2%E2%85%B1/</id>
    <published>2026-06-17T00:00:00.000Z</published>
    <updated>2026-06-17T00:00:00.000Z</updated>
    <summary>LeetCode-3614</summary>
    <content type="html"><![CDATA[<h1>题目描述</h1>
<p><a href="https://leetcode.cn/problems/process-string-with-special-operations-ii/description/?envType=daily-question&amp;envId=2026-06-17">题目链接</a></p>
<p>给你一个字符串 <code>s</code>，由小写英文字母和特殊字符 <code>*</code>、<code>#</code> 和 <code>%</code> 组成。</p>
<p>同时给你一个整数 <code>k</code>。</p>
<p>请根据以下规则从左到右处理 <code>s</code> 中的字符，构造一个新的字符串 <code>result</code>：</p>
<ul>
<li>如果字符是<strong>小写</strong>英文字母，则将其添加到 <code>result</code> 中。</li>
<li>字符 <code>'*'</code> 会<strong>删除</strong> <code>result</code> 中的最后一个字符（如果存在）。</li>
<li>字符 <code>'#'</code> 会<strong>复制</strong>当前的 <code>result</code> 并<strong>追加</strong>到其自身后面。</li>
<li>字符 <code>'%'</code> 会<strong>反转</strong>当前的 <code>result</code>。</li>
</ul>
<p>返回最终字符串 <code>result</code> 中第 <code>k</code> 个字符（下标从 0 开始）。如果 <code>k</code> 超出 <code>result</code> 的下标索引范围，则返回 <code>'.'</code>。</p>
<h2>输入</h2>
<p>字符串 <code>s</code>，整数 <code>k</code></p>
<h2>输出</h2>
<p><code>result[k]</code>，若越界则返回 <code>'.'</code></p>
<h2>约束条件</h2>
<ol>
<li><code>1 &lt;= s.length &lt;= 10^5</code></li>
<li><code>s</code> 只包含小写英文字母和特殊字符 <code>*</code>、<code>#</code> 和 <code>%</code></li>
<li><code>0 &lt;= k &lt;= 10^15</code></li>
<li>处理 <code>s</code> 后得到的 <code>result</code> 的长度不超过 <code>10^15</code></li>
</ol>
<h2>示例1</h2>
<pre><code>input: s = "a#b%*", k = 1
output: "a"
</code></pre>
<p>解释：依次得到 <code>"a"</code> → <code>"aa"</code> → <code>"aab"</code> → <code>"baa"</code> → <code>"ba"</code>，下标 <code>k = 1</code> 的字符是 <code>'a'</code></p>
<h2>示例2</h2>
<pre><code>input: s = "cd%#*#", k = 3
output: "d"
</code></pre>
<p>解释：依次得到 <code>"c"</code> → <code>"cd"</code> → <code>"dc"</code> → <code>"dcdc"</code> → <code>"dcd"</code> → <code>"dcddcd"</code>，下标 <code>k = 3</code> 的字符是 <code>'d'</code></p>
<h2>示例3</h2>
<pre><code>input: s = "z*#", k = 0
output: "."
</code></pre>
<p>解释：依次得到 <code>"z"</code> → <code>""</code> → <code>""</code>，<code>result</code> 为空串，<code>k = 0</code> 越界，返回 <code>'.'</code></p>
<h1>解题思路</h1>
<p>这题与 <a href="https://leetcode.cn/problems/process-string-with-special-operations-i/">用特殊操作处理字符串Ⅰ</a> 的处理规则完全相同，区别在于：Ⅰ 要求返回完整的 <code>result</code>，Ⅱ 只要求返回下标 <code>k</code> 处的<strong>一个字符</strong>。</p>
<h2>从模拟出发</h2>
<p>Ⅰ 的做法很直接——用列表维护 <code>result</code>，从左到右模拟四种操作即可。若把同样思路搬到这里，在 <code>s</code> 较短时确实能过：</p>
<pre><code>class Solution:
    def processStr(self, s: str, k: int) -&gt; str:
        res = []
        for c in s:
            if c == '*':
                if res: res.pop()
            elif c == '#': res += res
            elif c == '%': res.reverse()
            else: res.append(c)
        return res[k] if k &lt; len(res) else '.'
</code></pre>
<p>但本题 <code>result</code> 的长度可达 $10^{15}$，<code>'#'</code> 每次会把长度翻倍，字符串根本无法在内存中构造出来，模拟这条路走不通。</p>
<p>不过我们并不需要整个 <code>result</code>——只要第 <code>k</code> 个字符。能否在<strong>不展开字符串</strong>的前提下，把 <code>k</code> 映射回某个原始字母呢？</p>
<h2>正向：只算长度</h2>
<p>构造完整字符串做不到，但<strong>追踪长度</strong>可以。从左到右扫一遍 <code>s</code>，用变量 <code>sz</code> 表示当前 <code>result</code> 的长度：</p>
<ul>
<li>普通字母：<code>sz += 1</code></li>
<li><code>'*'</code>：删除末尾，<code>sz = max(sz - 1, 0)</code></li>
<li><code>'#'</code>：复制并拼接，<code>sz += sz</code>（长度翻倍）</li>
<li><code>'%'</code>：反转不改变长度，跳过</li>
</ul>
<p>扫完后 <code>sz</code> 就是最终 <code>result</code> 的长度。若 <code>k &gt;= sz</code>，直接返回 <code>'.'</code>。</p>
<p>以示例 1 为例，<code>"a#b%*"</code> 的长度变化为 $1 \to 2 \to 3 \to 3 \to 2$，最终 <code>sz = 2</code>，<code>k = 1</code> 合法。</p>
<h2>反向：把 k 映射回去</h2>
<p>正向知道了「最终有多长」，接下来从<strong>右往左</strong>逆推每个操作对下标 <code>k</code> 的影响，同时维护「当前这一步操作之后 <code>result</code> 的长度」<code>sz</code>（从最终长度出发，逐步还原到操作前的长度）。</p>
<p>逆推时，四种操作的「撤销」规则如下：</p>
<p><strong><code>'*'</code>（撤销删除）</strong><br />
正向删除使长度减 1，撤销则 <code>sz += 1</code>。</p>
<p><strong><code>'#'</code>（撤销复制）</strong><br />
正向把长度为 <code>sz/2</code> 的串复制成 <code>sz</code>，撤销先 <code>sz //= 2</code>。复制后 <code>result = 前半 + 后半</code>，若 <code>k</code> 落在后半段（<code>k &gt;= sz</code>），则 <code>k -= sz</code>，映射到前半段对应位置。</p>
<p><strong><code>'%'</code>（撤销反转）</strong><br />
反转不改变长度，但下标镜像：<code>k = sz - k - 1</code>。</p>
<p><strong>普通字母</strong><br />
正向追加使长度加 1，撤销则 <code>sz -= 1</code>。若此时 <code>k == sz</code>，说明目标字符就是当前这个字母，直接返回。</p>
<p>以示例 1 演示逆推过程。最终 <code>sz = 2</code>，<code>k = 1</code>，从右往左处理 <code>s</code>：</p>
<table>
<thead>
<tr>
<th>字符</th>
<th>操作</th>
<th>变化后 <code>sz</code></th>
<th>变化后 <code>k</code></th>
</tr>
</thead>
<tbody>
<tr>
<td><code>'*'</code></td>
<td>撤销删除</td>
<td>3</td>
<td>1</td>
</tr>
<tr>
<td><code>'%'</code></td>
<td>撤销反转</td>
<td>3</td>
<td>$3-1-1=1$</td>
</tr>
<tr>
<td><code>'b'</code></td>
<td>撤销追加</td>
<td>2</td>
<td>1（不等于 2，继续）</td>
</tr>
<tr>
<td><code>'#'</code></td>
<td>撤销复制</td>
<td>1</td>
<td>$1-1=0$（$k \ge sz$，减去前半段长度）</td>
</tr>
<tr>
<td><code>'a'</code></td>
<td>撤销追加</td>
<td>0</td>
<td>$k=0=sz$，返回 <code>'a'</code></td>
</tr>
</tbody>
</table>
<p>示例 2 中 <code>s = "cd%#*#"</code>，正向得 <code>sz = 6</code>，<code>k = 3</code>。逆推时遇到 <code>'#'</code> 会将 <code>k</code> 从 3 调整为 0，再经 <code>'%'</code> 镜像为 1，最终在字母 <code>'d'</code> 处命中。</p>
<h2>复杂度</h2>
<p>正向、反向各遍历 <code>s</code> 一次，时间复杂度 $O(n)$，空间复杂度 $O(1)$。</p>
<h2>代码</h2>
<pre><code>class Solution:
    def processStr(self, s: str, k: int) -&gt; str:
        sz = 0
        for c in s:
            if c == '*':
                sz = max(sz - 1, 0)
            elif c == '#':
                sz += sz
            elif c == '%': continue
            else: sz += 1
        
        if k &gt;= sz: return '.'

        for c in reversed(s):
            if c == '*': sz += 1
            elif c == '#':
                sz //= 2
                if k &gt;= sz: k -= sz
            elif c == '%': 
                k = sz - k - 1
            else: 
                sz -= 1
                if k == sz: return c
</code></pre>
]]></content>
    <author><name>Hygen</name></author>
    <category term="Algorithm and Structure"/>
  </entry>
  <entry>
    <title>移动零</title>
    <link href="https://www.hygen.red/posts/hot100/02-%E5%8F%8C%E6%8C%87%E9%92%88/004%E7%A7%BB%E5%8A%A8%E9%9B%B6/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/hot100/02-%E5%8F%8C%E6%8C%87%E9%92%88/004%E7%A7%BB%E5%8A%A8%E9%9B%B6/</id>
    <published>2026-06-17T00:00:00.000Z</published>
    <updated>2026-06-17T00:00:00.000Z</updated>
    <summary>Leetcode-283</summary>
    <content type="html"><![CDATA[<h1>题目描述</h1>
<p><a href="https://leetcode.cn/problems/move-zeroes/?envType=study-plan-v2&amp;envId=top-100-liked">题目链接</a></p>
<p>给定一个数组 <code>nums</code>，编写一个函数将所有 <code>0</code> 移动到数组的末尾，同时保持非零元素的相对顺序。</p>
<p><strong>请注意</strong>，必须在不复制数组的情况下原地对数组进行操作。</p>
<h2>输入</h2>
<p>整数数组 <code>nums</code></p>
<h2>输出</h2>
<p>无返回值，原地修改 <code>nums</code></p>
<h2>约束条件</h2>
<ol>
<li><code>1 &lt;= nums.length &lt;= 10^4</code></li>
<li><code>-2^31 &lt;= nums[i] &lt;= 2^31 - 1</code></li>
</ol>
<h1>进阶约束</h1>
<ol>
<li>能否尽量减少操作次数？</li>
</ol>
<h2>示例1</h2>
<pre><code>input: nums = [0,1,0,3,12]
output: [1,3,12,0,0]
</code></pre>
<h2>示例2</h2>
<pre><code>input: nums = [0]
output: [0]
</code></pre>
<h1>解题思路</h1>
<p>题目要求把数组中的 <code>0</code> 全部挪到末尾，同时<strong>非零元素之间的相对顺序不能变</strong>，并且必须<strong>原地</strong>修改，不能另开数组。</p>
<h2>从直观做法出发</h2>
<p>最容易想到的是：先统计 <code>0</code> 的个数，再把非零元素按顺序填到前面，最后把剩余位置全部填 <code>0</code>。思路没问题，但通常需要额外数组或两次遍历，不够简洁。</p>
<p>另一种想法是：从左到右扫描，每遇到一个非零元素，就把它「写」到当前应该存放的位置。非零元素按扫描顺序依次写入，空出来的位置自然就是 <code>0</code> 了——这正是双指针能做的事。</p>
<h2>双指针：一个扫描，一个写入</h2>
<p>用两个指针 <code>i</code> 和 <code>j</code>，初始都为 <code>0</code>：</p>
<ul>
<li><code>i</code>：从左到右扫描整个数组</li>
<li><code>j</code>：指向下一个非零元素应该写入的位置</li>
</ul>
<p>遍历过程中，若 <code>nums[i]</code> 非零，就把它交换到 <code>nums[j]</code>，然后 <code>j</code> 加一；若 <code>nums[i]</code> 为零，则什么都不做，继续让 <code>i</code> 往后走。</p>
<p>以 <code>nums = [0, 1, 0, 3, 12]</code> 为例：</p>
<table>
<thead>
<tr>
<th><code>i</code></th>
<th><code>nums[i]</code></th>
<th>操作</th>
<th><code>nums</code></th>
<th><code>j</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>0</td>
<td>跳过</td>
<td><code>[0,1,0,3,12]</code></td>
<td>0</td>
</tr>
<tr>
<td>1</td>
<td>1</td>
<td>交换 <code>nums[1]</code> 与 <code>nums[0]</code></td>
<td><code>[1,0,0,3,12]</code></td>
<td>1</td>
</tr>
<tr>
<td>2</td>
<td>0</td>
<td>跳过</td>
<td><code>[1,0,0,3,12]</code></td>
<td>1</td>
</tr>
<tr>
<td>3</td>
<td>3</td>
<td>交换 <code>nums[3]</code> 与 <code>nums[1]</code></td>
<td><code>[1,3,0,0,12]</code></td>
<td>2</td>
</tr>
<tr>
<td>4</td>
<td>12</td>
<td>交换 <code>nums[4]</code> 与 <code>nums[2]</code></td>
<td><code>[1,3,12,0,0]</code></td>
<td>3</td>
</tr>
</tbody>
</table>
<p>遍历结束后，<code>j</code> 之后的元素本来就是 <code>0</code> 或被交换过来的 <code>0</code>，上表最后一行即为最终结果。</p>
<p>当 <code>i == j</code> 时交换等于没交换，因此非零元素在「前面没有零」的情况下不会被多余地移动，相对顺序也始终得以保持。</p>
<h2>复杂度</h2>
<ul>
<li>时间复杂度：$O(n)$，只遍历数组一次</li>
<li>空间复杂度：$O(1)$，仅使用常数额外空间</li>
</ul>
<h2>代码</h2>
<pre><code>class Solution:
    def moveZeroes(self, nums: List[int]) -&gt; None:
        """
        Do not return anything, modify nums in-place instead.
        """
        i = j = 0
        for i in range(len(nums)):
            if nums[i]:
                nums[i], nums[j] = nums[j], nums[i]
                j += 1
</code></pre>
]]></content>
    <author><name>Hygen</name></author>
    <category term="hot100"/>
  </entry>
  <entry>
    <title>盛水最多的容器</title>
    <link href="https://www.hygen.red/posts/hot100/02-%E5%8F%8C%E6%8C%87%E9%92%88/005%E7%9B%9B%E6%B0%B4%E6%9C%80%E5%A4%9A%E7%9A%84%E5%AE%B9%E5%99%A8/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/hot100/02-%E5%8F%8C%E6%8C%87%E9%92%88/005%E7%9B%9B%E6%B0%B4%E6%9C%80%E5%A4%9A%E7%9A%84%E5%AE%B9%E5%99%A8/</id>
    <published>2026-06-17T00:00:00.000Z</published>
    <updated>2026-06-17T00:00:00.000Z</updated>
    <summary>Leetcode-11</summary>
    <content type="html"><![CDATA[<h1>题目描述</h1>
<p><a href="https://leetcode.cn/problems/container-with-most-water/?envType=study-plan-v2&amp;envId=top-100-liked">题目链接</a></p>
<p>给定一个长度为 <code>n</code> 的整数数组 <code>height</code>。有 <code>n</code> 条垂线，第 <code>i</code> 条线的两个端点是 <code>(i, 0)</code> 和 <code>(i, height[i])</code>。</p>
<p>找出其中的两条线，使得它们与 <code>x</code> 轴共同构成的容器可以容纳最多的水。</p>
<p>返回容器可以储存的最大水量。</p>
<p><strong>说明：</strong> 你不能倾斜容器。</p>
<h2>输入</h2>
<p>整数数组 <code>height</code></p>
<h2>输出</h2>
<p>容器可以储存的最大水量（整数）</p>
<h2>约束条件</h2>
<ol>
<li><code>n == height.length</code></li>
<li><code>2 &lt;= n &lt;= 10^5</code></li>
<li><code>0 &lt;= height[i] &lt;= 10^4</code></li>
</ol>
<h1>进阶约束</h1>
<ol>
<li>能否将时间复杂度优化到 $O(n)$？</li>
</ol>
<h2>示例1</h2>
<pre><code>input: height = [1,8,6,2,5,4,8,3,7]
output: 49
</code></pre>
<p>解释：选择下标 <code>1</code> 和 <code>8</code> 的两条线，高度为 <code>8</code> 和 <code>7</code>，宽度为 <code>7</code>，面积为 <code>min(8, 7) × 7 = 49</code>。</p>
<h2>示例2</h2>
<pre><code>input: height = [1,1]
output: 1
</code></pre>
<h1>解题思路</h1>
<p>题目本质是：在数组中选两个下标 <code>i &lt; j</code>，以它们为左右边界、以 <code>min(height[i], height[j])</code> 为「短板」高度，计算容器面积 <code>min(height[i], height[j]) × (j - i)</code>，求最大值。</p>
<h2>从暴力枚举出发</h2>
<p>最直观的做法是枚举所有 <code>(i, j)</code> 配对，逐一计算面积并取最大值：</p>
<pre><code>class Solution:
    def maxArea(self, height: List[int]) -&gt; int:
        ans = 0
        n = len(height)
        for i in range(n):
            for j in range(i + 1, n):
                ans = max(ans, min(height[i], height[j]) * (j - i))
        return ans
</code></pre>
<p>思路正确，但时间复杂度为 $O(n^2)$，在 $n$ 可达 $10^5$ 时会超时。我们需要找到一种能<strong>跳过大量无效配对</strong>的方法。</p>
<h2>观察：宽度与高度的取舍</h2>
<p>面积由两个因素决定：</p>
<ul>
<li><strong>宽度</strong> <code>(j - i)</code>：两指针越靠近，宽度越小</li>
<li><strong>高度</strong> <code>min(height[i], height[j])</code>：由较短的那条边决定（木桶效应）</li>
</ul>
<p>若固定左右边界后向内收缩，宽度必然减小。此时若想面积变大，只能指望高度增加——但高度受短板限制，<strong>移动较高的一边</strong>只会让宽度变小，而高度仍被原来的短板卡住，面积一定变小，可以直接排除。</p>
<p>反过来，<strong>移动较短的一边</strong>虽然宽度也减小了，但有机会遇到更高的线，从而抬高短板，面积仍有可能变大。</p>
<h2>双指针：从两端向中间收缩</h2>
<p>基于上述观察，用两个指针 <code>l</code> 和 <code>r</code> 分别指向数组首尾，初始时宽度最大：</p>
<ol>
<li>计算当前面积 <code>min(height[l], height[r]) × (r - l)</code>，更新答案</li>
<li>比较 <code>height[l]</code> 与 <code>height[r]</code>，将<strong>较短一侧</strong>的指针向内移动一步</li>
<li>重复直到 <code>l &gt;= r</code></li>
</ol>
<p>以 <code>height = [1, 8, 6, 2, 5, 4, 8, 3, 7]</code> 为例，前几步如下：</p>
<table>
<thead>
<tr>
<th><code>l</code></th>
<th><code>r</code></th>
<th><code>height[l]</code></th>
<th><code>height[r]</code></th>
<th>面积</th>
<th>移动</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>8</td>
<td>1</td>
<td>7</td>
<td>1 × 8 = 8</td>
<td><code>l</code> 右移（左侧更短）</td>
</tr>
<tr>
<td>1</td>
<td>8</td>
<td>8</td>
<td>7</td>
<td>7 × 7 = <strong>49</strong></td>
<td><code>r</code> 左移（右侧更短）</td>
</tr>
<tr>
<td>1</td>
<td>7</td>
<td>8</td>
<td>3</td>
<td>3 × 6 = 18</td>
<td><code>r</code> 左移</td>
</tr>
<tr>
<td>1</td>
<td>6</td>
<td>8</td>
<td>8</td>
<td>8 × 5 = 40</td>
<td>相等，移动 <code>l</code> 或 <code>r</code> 均可</td>
</tr>
</tbody>
</table>
<p>遍历过程中记录到的最大面积即为 <code>49</code>。</p>
<p>为什么这样不会漏掉最优解？假设最优配对为 <code>(i, j)</code>，当指针尚未到达它们时，若较短边在外侧，双指针会不断将其向内推；一旦某一侧指针越过 <code>i</code> 或 <code>j</code>，说明所有以该外侧边为短板的更宽配对都已检查完毕。每个指针最多移动 $n - 1$ 次，因此总共 $O(n)$ 步。</p>
<h2>复杂度</h2>
<ul>
<li>时间复杂度：$O(n)$，两个指针各最多移动 $n - 1$ 次</li>
<li>空间复杂度：$O(1)$，仅使用常数额外空间</li>
</ul>
<h2>代码</h2>
<pre><code>class Solution:
    def maxArea(self, height: List[int]) -&gt; int:
        ans = 0
        l, r = 0, len(height) - 1
        while l &lt; r:
            ans = max(ans, min(height[l], height[r]) * (r - l))
            if height[l] &lt; height[r]: l += 1
            else: r -= 1

        return ans
</code></pre>
<h1>代码优化</h1>
<p>本题在算法层面，双指针 $O(n)$ 已是时间最优——不存在比线性更低的下界（至少要看一遍数组）。因此优化空间主要在代码层面。</p>
<h2>合并相等时的分支</h2>
<p>当 <code>height[l] == height[r]</code> 时，移动左指针或右指针效果等价：宽度都减 1，且两侧高度相同，新面积不可能超过当前。将条件改为 <code>&lt;=</code> 可统一写法，避免 <code>if-else</code> 两路分支：</p>
<pre><code>class Solution:
    def maxArea(self, height: List[int]) -&gt; int:
        ans = 0
        l, r = 0, len(height) - 1
        while l &lt; r:
            ans = max(ans, min(height[l], height[r]) * (r - l))
            if height[l] &lt;= height[r]:
                l += 1
            else:
                r -= 1
        return ans
</code></pre>
<h2>减少重复下标访问</h2>
<p>在循环较密集时，可将两侧高度先存入局部变量，避免多次通过下标读取数组（对 Python 而言提升有限，但意图更清晰）：</p>
<pre><code>class Solution:
    def maxArea(self, height: List[int]) -&gt; int:
        ans = 0
        l, r = 0, len(height) - 1
        while l &lt; r:
            hl, hr = height[l], height[r]
            ans = max(ans, min(hl, hr) * (r - l))
            if hl &lt;= hr:
                l += 1
            else:
                r -= 1
        return ans
</code></pre>
]]></content>
    <author><name>Hygen</name></author>
    <category term="hot100"/>
  </entry>
  <entry>
    <title>三数之和</title>
    <link href="https://www.hygen.red/posts/hot100/02-%E5%8F%8C%E6%8C%87%E9%92%88/006%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/hot100/02-%E5%8F%8C%E6%8C%87%E9%92%88/006%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C/</id>
    <published>2026-06-17T00:00:00.000Z</published>
    <updated>2026-06-17T00:00:00.000Z</updated>
    <summary>Leetcode-15</summary>
    <content type="html"><![CDATA[<h1>题目描述</h1>
<p><a href="https://leetcode.cn/problems/3sum/?envType=study-plan-v2&amp;envId=top-100-liked">题目链接</a></p>
<p>给你一个整数数组 <code>nums</code>，判断是否存在三元组 <code>[nums[i], nums[j], nums[k]]</code> 满足 <code>i != j</code>、<code>i != k</code> 且 <code>j != k</code>，同时还满足 <code>nums[i] + nums[j] + nums[k] == 0</code>。请你返回所有和为 <code>0</code> 且不重复的三元组。</p>
<p><strong>注意：</strong> 答案中不可以包含重复的三元组。</p>
<h2>输入</h2>
<p>整数数组 <code>nums</code></p>
<h2>输出</h2>
<p>所有和为 <code>0</code> 且不重复的三元组列表</p>
<h2>约束条件</h2>
<ol>
<li><code>3 &lt;= nums.length &lt;= 3000</code></li>
<li><code>-10^5 &lt;= nums[i] &lt;= 10^5</code></li>
</ol>
<h1>进阶约束</h1>
<ol>
<li>输出中不得包含重复的三元组</li>
</ol>
<h2>示例1</h2>
<pre><code>input: nums = [-1,0,1,2,-1,-4]
output: [[-1,-1,2],[-1,0,1]]
</code></pre>
<p>解释：和为 <code>0</code> 的三元组有 <code>[-1, 0, 1]</code> 与 <code>[-1, -1, 2]</code>，输出顺序不限。</p>
<h2>示例2</h2>
<pre><code>input: nums = [0,1,1]
output: []
</code></pre>
<h2>示例3</h2>
<pre><code>input: nums = [0,0,0]
output: [[0,0,0]]
</code></pre>
<h1>解题思路</h1>
<p>题目本质是：在数组中找出所有<strong>不重复</strong>的三元组，使其元素之和为 <code>0</code>。难点不在于「找不找得到」，而在于<strong>去重</strong>——同一个三元组可能对应多组下标，答案里只能出现一次。</p>
<h2>从暴力枚举出发</h2>
<p>最直观的做法是三层循环，枚举所有 <code>(i, j, k)</code> 组合，检查三数之和是否为 <code>0</code>：</p>
<pre><code>class Solution:
    def threeSum(self, nums: list[int]) -&gt; list[list[int]]:
        res = []
        n = len(nums)
        for i in range(n):
            for j in range(i + 1, n):
                for k in range(j + 1, n):
                    if nums[i] + nums[j] + nums[k] == 0:
                        res.append(sorted([nums[i], nums[j], nums[k]]))
        return list({tuple(t) for t in res})  # 借助 set 去重
</code></pre>
<p>思路正确，但时间复杂度为 $O(n^3)$，在 $n$ 可达 $3000$ 时会超时；末尾还要额外去重，写法也繁琐。我们需要减少枚举量，并更优雅地处理重复。</p>
<h2>降维：固定一个数，转化为两数之和</h2>
<p>若固定第一个数 <code>nums[i]</code>，问题就退化为：在剩余元素中找两个数，使三数之和为 <code>0</code>，即找 <code>nums[j] + nums[k] = -nums[i]</code>——这正是经典的<strong>两数之和</strong>。</p>
<p>两数之和可以用哈希表 $O(n)$ 求解，但本题要返回<strong>全部</strong>不重复三元组，哈希法去重较麻烦。更自然的做法是：<strong>先排序，再用双指针</strong>在有序数组上 $O(n)$ 找一对数。</p>
<h2>先排序</h2>
<p>对 <code>nums</code> 排序后有两个好处：</p>
<ol>
<li><strong>双指针可行</strong>：有序数组上，左右指针向内移动时，三数之和单调变化，可以决定移动哪一侧</li>
<li><strong>去重简单</strong>：相同元素相邻排列，跳过重复值即可</li>
</ol>
<pre><code>nums.sort()
</code></pre>
<h2>外层枚举第一个数</h2>
<p>用下标 <code>i</code> 枚举三元组的第一个元素，范围是 <code>[0, n - 3]</code>（后面至少还要留两个位置给 <code>j</code> 和 <code>k</code>）。</p>
<h3>跳过重复的 <code>nums[i]</code></h3>
<p>若 <code>nums[i]</code> 与 <code>nums[i - 1]</code> 相同，以它为第一个数得到的三元组必然重复，直接 <code>continue</code>：</p>
<pre><code>if i and nums[i] == nums[i - 1]:
    continue
</code></pre>
<h3>剪枝：提前结束或跳过</h3>
<p>数组已有序，可以利用边界值快速判断当前 <code>i</code> 是否还有希望：</p>
<ul>
<li><code>nums[i] + nums[i+1] + nums[i+2] &gt; 0</code>：最小的三个数之和已大于 <code>0</code>，后面 <code>i</code> 更大只会更糟，<strong>直接 <code>break</code></strong></li>
<li><code>nums[i] + nums[-1] + nums[-2] &lt; 0</code>：即使配上最大的两个数仍小于 <code>0</code>，当前 <code>i</code> 不可能有解，<strong><code>continue</code> 换下一个 <code>i</code></strong></li>
</ul>
<h2>内层双指针：在 <code>[i+1, n-1]</code> 上找两数之和</h2>
<p>设 <code>j = i + 1</code>、<code>k = n - 1</code>，在 <code>j &lt; k</code> 时计算 <code>cnt = nums[i] + nums[j] + nums[k]</code>：</p>
<table>
<thead>
<tr>
<th>情况</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>cnt &lt; 0</code></td>
<td>和太小，<code>j</code> 右移，尝试更大的 <code>nums[j]</code></td>
</tr>
<tr>
<td><code>cnt &gt; 0</code></td>
<td>和太大，<code>k</code> 左移，尝试更小的 <code>nums[k]</code></td>
</tr>
<tr>
<td><code>cnt == 0</code></td>
<td>找到一组解，记录后 <code>j</code>、<code>k</code> 同时向内移动</td>
</tr>
</tbody>
</table>
<p>以 <code>nums = [-4, -1, -1, 0, 1, 2]</code>（排序后）为例，当 <code>i = 1</code>（<code>nums[i] = -1</code>）时：</p>
<table>
<thead>
<tr>
<th><code>j</code></th>
<th><code>k</code></th>
<th><code>cnt</code></th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>2</td>
<td>5</td>
<td>-1 + (-1) + 2 = 0</td>
<td>记录 <code>[-1, -1, 2]</code>，<code>j</code>、<code>k</code> 内移并去重</td>
</tr>
<tr>
<td>3</td>
<td>4</td>
<td>-1 + 0 + 1 = 0</td>
<td>记录 <code>[-1, 0, 1]</code>，<code>j</code>、<code>k</code> 内移</td>
</tr>
</tbody>
</table>
<h3>找到解后跳过重复的 <code>j</code>、<code>k</code></h3>
<p>命中一组解后，<code>j</code> 和 <code>k</code> 各自跳过与当前值相同的元素，避免重复三元组：</p>
<pre><code>while j &lt; k and nums[j] == nums[j - 1]:
    j += 1
while j &lt; k and nums[k] == nums[k + 1]:
    k -= 1
</code></pre>
<h2>复杂度</h2>
<ul>
<li>时间复杂度：$O(n^2)$。排序 $O(n \log n)$，外层 <code>i</code> 遍历 $O(n)$，内层双指针总共移动 $O(n)$ 次</li>
<li>空间复杂度：$O(1)$（不计排序栈空间和输出数组）</li>
</ul>
<h2>代码</h2>
<pre><code>class Solution:
    def threeSum(self, nums: list[int]) -&gt; list[list[int]]:
        nums.sort()
        res = []
        n = len(nums)
        for i in range(n - 2):
            if i and nums[i] == nums[i - 1]: continue
            if nums[i] + nums[i + 1] + nums[i + 2] &gt; 0: break
            if nums[i] + nums[-1] + nums[-2] &lt; 0: continue
            j, k = i + 1, n - 1
            while j &lt; k:
                cnt = nums[i] + nums[j] + nums[k]
                if cnt &lt; 0: j += 1
                elif cnt &gt; 0: k -= 1
                else:
                    res.append([nums[i], nums[j], nums[k]])
                    j += 1
                    while j &lt; k and nums[j] == nums[j - 1]: j += 1
                    k -= 1
                    while j &lt; k and nums[k] == nums[k + 1]: k -= 1

        return res
</code></pre>
<h1>解题思路（哈希表）</h1>
<p>排序 + 双指针已是本题的主流最优解，时间 $O(n^2)$、空间 $O(1)$。另一种常见思路是<strong>不排序，用哈希表做两数之和</strong>：</p>
<ol>
<li>外层仍枚举第一个数 <code>nums[i]</code></li>
<li>内层用哈希集合 <code>seen</code> 记录已遍历过的 <code>nums[j]</code></li>
<li>对每个 <code>j</code>，目标值为 <code>target = -nums[i] - nums[j]</code>；若 <code>target in seen</code>，说明之前某处出现过补数，可组成三元组</li>
<li>将 <code>nums[j]</code> 加入 <code>seen</code></li>
</ol>
<p>去重策略：对外层 <code>i</code> 跳过重复值；命中解后将三元组排序后存入 <code>set</code>，最后转回列表。</p>
<pre><code>class Solution:
    def threeSum(self, nums: list[int]) -&gt; list[list[int]]:
        res = set()
        n = len(nums)
        for i in range(n - 2):
            if i and nums[i] == nums[i - 1]:
                continue
            seen = set()
            for j in range(i + 1, n):
                complement = -nums[i] - nums[j]
                if complement in seen:
                    res.add(tuple(sorted((nums[i], complement, nums[j]))))
                seen.add(nums[j])
        return [list(t) for t in res]
</code></pre>
<table>
<thead>
<tr>
<th>对比项</th>
<th>排序 + 双指针</th>
<th>哈希表</th>
</tr>
</thead>
<tbody>
<tr>
<td>时间</td>
<td>$O(n^2)$</td>
<td>$O(n^2)$</td>
</tr>
<tr>
<td>空间</td>
<td>$O(1)$</td>
<td>$O(n)$</td>
</tr>
<tr>
<td>去重</td>
<td>利用有序性，逻辑清晰</td>
<td>需借助 <code>set</code> + 排序元组</td>
</tr>
<tr>
<td>剪枝</td>
<td>边界剪枝效果好</td>
<td>较难做同类剪枝</td>
</tr>
</tbody>
</table>
<p>面试和竞赛中更推荐排序 + 双指针：空间更优、去重更自然，也便于扩展到「三数之和最接近」「四数之和」等变形题。</p>
<h1>代码优化</h1>
<p>本题在算法层面，$O(n^2)$ 已接近最优——最坏情况下合法三元组数量可达 $O(n^2)$，输出本身就需要 $O(n^2)$ 时间。因此优化空间主要在<strong>剪枝</strong>与<strong>代码可读性</strong>。</p>
<h2>缓存 <code>nums[i]</code>，减少重复下标访问</h2>
<p>外层循环中多次用到 <code>nums[i]</code>，可先存入局部变量：</p>
<pre><code>for i in range(n - 2):
    x = nums[i]
    if i and x == nums[i - 1]:
        continue
    if x + nums[i + 1] + nums[i + 2] &gt; 0:
        break
    if x + nums[-1] + nums[-2] &lt; 0:
        continue
    # 内层用 x 代替 nums[i]
</code></pre>
<h2>抽取「跳过重复值」为内联模式</h2>
<p>找到解后 <code>j</code>、<code>k</code> 的去重逻辑对称，可先各自前进一步再统一跳过：</p>
<pre><code>else:
    res.append([nums[i], nums[j], nums[k]])
    j += 1
    k -= 1
    while j &lt; k and nums[j] == nums[j - 1]:
        j += 1
    while j &lt; k and nums[k] == nums[k + 1]:
        k -= 1
</code></pre>
<p>这与原写法等价，但将「记录解」与「移动指针」和「去重」三步分开，阅读时层次更清楚。</p>
<h2>命中解时双指针同时内移</h2>
<p><code>cnt == 0</code> 时 <code>j += 1</code> 与 <code>k -= 1</code> 必须<strong>都执行</strong>——无论 <code>nums[j]</code> 与 <code>nums[k]</code> 是否相等，当前这对 <code>(j, k)</code> 已被消费，只移动一侧会重复枚举同一对。原代码在 <code>j += 1</code> 后做 <code>j</code> 去重、再 <code>k -= 1</code> 后做 <code>k</code> 去重，顺序正确，是常见写法。</p>
]]></content>
    <author><name>Hygen</name></author>
    <category term="hot100"/>
  </entry>
  <entry>
    <title>用特殊操作处理字符串Ⅰ</title>
    <link href="https://www.hygen.red/posts/dsa/solution/006%E7%94%A8%E7%89%B9%E6%AE%8A%E6%93%8D%E4%BD%9C%E5%A4%84%E7%90%86%E5%AD%97%E7%AC%A6%E4%B8%B2%E2%85%B0/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/dsa/solution/006%E7%94%A8%E7%89%B9%E6%AE%8A%E6%93%8D%E4%BD%9C%E5%A4%84%E7%90%86%E5%AD%97%E7%AC%A6%E4%B8%B2%E2%85%B0/</id>
    <published>2026-06-16T00:00:00.000Z</published>
    <updated>2026-06-16T00:00:00.000Z</updated>
    <summary>LeetCode-3612</summary>
    <content type="html"><![CDATA[<h1>题目描述</h1>
<p><a href="https://leetcode.cn/problems/process-string-with-special-operations-i/description/?envType=daily-question&amp;envId=2026-06-16">题目链接</a></p>
<p>给你一个字符串 <code>s</code>，它由小写英文字母和特殊字符 <code>*</code>、<code>#</code> 和 <code>%</code> 组成。</p>
<p>请根据以下规则从左到右处理 <code>s</code> 中的字符，构造一个新的字符串 <code>result</code>：</p>
<ul>
<li>如果字符是<strong>小写</strong>英文字母，则将其添加到 <code>result</code> 中。</li>
<li>字符 <code>'*'</code> 会<strong>删除</strong> <code>result</code> 中的最后一个字符（如果存在）。</li>
<li>字符 <code>'#'</code> 会<strong>复制</strong>当前的 <code>result</code> 并<strong>追加</strong>到其自身后面。</li>
<li>字符 <code>'%'</code> 会<strong>反转</strong>当前的 <code>result</code>。</li>
</ul>
<p>在处理完 <code>s</code> 中的所有字符后，返回最终的字符串 <code>result</code>。</p>
<h2>输入</h2>
<p>字符串 <code>s</code></p>
<h2>输出</h2>
<p>按规则处理后的字符串 <code>result</code></p>
<h2>约束条件</h2>
<ol>
<li><code>1 &lt;= s.length &lt;= 20</code></li>
<li><code>s</code> 只包含小写英文字母和特殊字符 <code>*</code>、<code>#</code> 和 <code>%</code></li>
</ol>
<h2>示例1</h2>
<pre><code>input: s = "a#b%*"
output: "ba"
</code></pre>
<p>解释：依次得到 <code>"a"</code> → <code>"aa"</code> → <code>"aab"</code> → <code>"baa"</code> → <code>"ba"</code></p>
<h2>示例2</h2>
<pre><code>input: s = "z*#"
output: ""
</code></pre>
<p>解释：依次得到 <code>"z"</code> → <code>""</code> → <code>""</code></p>
<h1>解题思路</h1>
<p>题目要求从左到右依次处理每个字符，并根据当前字符对 <code>result</code> 做追加、删除、复制或反转。这类题没有更巧妙的数学转化，核心就是<strong>按题意模拟</strong>。</p>
<p>模拟时需要维护一个可变的中间结果。若直接用字符串拼接，遇到 <code>'*'</code> 删除末尾、<code>'%'</code> 反转时都要频繁切片或重建，写法繁琐。更自然的做法是使用列表 <code>res</code> 作为当前 <code>result</code> 的字符容器：</p>
<ul>
<li>普通字母：直接 <code>append</code>，等价于追加到末尾。</li>
<li><code>'*'</code>：若 <code>res</code> 非空则 <code>pop()</code>，删除最后一个字符。</li>
<li><code>'#'</code>：<code>res += res</code>，将当前内容整体复制一份拼到后面。</li>
<li><code>'%'</code>：<code>res.reverse()</code>，原地反转。</li>
</ul>
<p>遍历结束后 <code>''.join(res)</code> 即为答案。列表尾部增删是 $O(1)$，复制和反转与当前结果长度有关；在题目给定的数据规模下完全足够。</p>
<h2>代码</h2>
<pre><code>class Solution:
    def processStr(self, s: str) -&gt; str:
        res = []

        for c in s:
            if c == '*': 
                if res: res.pop()
            elif c == '#': res += res
            elif c == '%': res.reverse()
            else: res.append(c)

        return ''.join(res)
</code></pre>
]]></content>
    <author><name>Hygen</name></author>
    <category term="Algorithm and Structure"/>
  </entry>
  <entry>
    <title>最长连续序列</title>
    <link href="https://www.hygen.red/posts/hot100/01-%E5%93%88%E5%B8%8C/003%E6%9C%80%E9%95%BF%E8%BF%9E%E7%BB%AD%E5%BA%8F%E5%88%97/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/hot100/01-%E5%93%88%E5%B8%8C/003%E6%9C%80%E9%95%BF%E8%BF%9E%E7%BB%AD%E5%BA%8F%E5%88%97/</id>
    <published>2026-06-16T00:00:00.000Z</published>
    <updated>2026-06-16T00:00:00.000Z</updated>
    <summary>Leetcode-128</summary>
    <content type="html"><![CDATA[<h1>题目描述</h1>
<p><a href="https://leetcode.cn/problems/longest-consecutive-sequence/description/?envType=study-plan-v2&amp;envId=top-100-liked">题目链接</a></p>
<h2>输入</h2>
<p>一个未排序的整数数组 <code>nums</code></p>
<h2>输出</h2>
<p>数字连续的最长序列的长度（不要求序列元素在原数组中连续）</p>
<h2>约束条件</h2>
<ol>
<li><code>0 &lt;= nums.length &lt;= 10^5</code></li>
<li><code>-10^9 &lt;= nums[i] &lt;= 10^9</code></li>
<li>时间复杂度需要为 $O(n)$</li>
</ol>
<h2>示例1</h2>
<pre><code>input: nums = [100,4,200,1,3,2]
output: 4
</code></pre>
<p>解释：最长数字连续序列是 <code>[1, 2, 3, 4]</code>，长度为 4。</p>
<h2>示例2</h2>
<pre><code>input: nums = [0,3,7,2,5,8,4,6,0,1]
output: 9
</code></pre>
<p>解释：最长数字连续序列是 <code>[0, 1, 2, 3, 4, 5, 6, 7, 8]</code>，长度为 9。</p>
<h2>示例3</h2>
<pre><code>input: nums = [1,0,1,2]
output: 3
</code></pre>
<p>解释：最长数字连续序列是 <code>[0, 1, 2]</code>，长度为 3。</p>
<h1>解题思路</h1>
<p>题目要求找出数组中<strong>数值连续</strong>的最长序列长度，这里的"连续"指的是数字本身相差 1，并不要求它们在原数组中下标相邻。</p>
<p>很容易想到先排序再从头扫描统计连续段长度，比如 <code>[100,4,200,1,3,2]</code> 排序后变成 <code>[1,2,3,4,100,200]</code>，扫一遍就能数出最长段。但排序至少需要 $O(n \log n)$，题目明确要求 $O(n)$，此路不通。</p>
<p>既然不能排序，又要快速判断某个数是否在集合中，我们在前两题学过的<strong>哈希集合</strong>就派上用场了——<code>set</code> 的查询是 $O(1)$ 的，还能顺便去重。</p>
<p>一个自然的想法是：把 <code>nums</code> 转成 <code>set</code>，对每个数 <code>x</code> 不断检查 <code>x+1</code>、<code>x+2</code>……是否在集合里，统计长度。但这样会有大量重复计算——比如序列 <code>[1,2,3,4]</code>，从 1 往上数一遍，从 2 又往上数一遍，最坏会退化到 $O(n^2)$。</p>
<p>怎么避免重复？关键思路是：<strong>每条连续序列只需要统计一次</strong>。常见做法是从序列的<strong>左端点</strong>入手——若 <code>x - 1</code> 在集合中，说明 <code>x</code> 不是开头，直接跳过；否则从 <code>x</code> 向右扩展统计长度。</p>
<p>我们的做法是对称的：从序列的<strong>右端点</strong>入手。遍历集合中的每个 <code>x</code>，若 <code>x + 1</code> 还在集合里，说明 <code>x</code> 不是某段连续序列的最大值，跳过；只有当 <code>x + 1</code> 不存在时，<code>x</code> 才是一段连续序列的右端点。</p>
<p>确认 <code>x</code> 是右端点后，向左"顺藤摸瓜"：令 <code>t = x</code>，不断 <code>t -= 1</code>，直到 <code>t</code> 不在集合中为止。此时 <code>t</code> 恰好落在连续段左端点的左边一位，因此这段序列的长度就是 <code>x - t</code>。</p>
<p>以 <code>nums = [100,4,200,1,3,2]</code> 为例，去重后集合为 <code>{1,2,3,4,100,200}</code>：</p>
<ul>
<li><code>1</code>、<code>2</code>、<code>3</code> 的右邻 <code>+1</code> 都在集合中，跳过</li>
<li>遇到 <code>4</code>：<code>5</code> 不在集合中，向左走 <code>4→3→2→1→0</code>，<code>t</code> 停在 <code>0</code>，长度 <code>4 - 0 = 4</code></li>
<li><code>100</code>、<code>200</code> 各自成段，长度均为 1</li>
</ul>
<p>每条连续序列只在处理其最大值时统计一次，每个数最多被向左访问一次，总时间复杂度 $O(n)$，空间复杂度 $O(n)$。</p>
<h2>代码</h2>
<pre><code>class Solution:
    def longestConsecutive(self, nums: List[int]) -&gt; int:
        nums = set(nums)
        ans = 0
        for x in nums:
            if x + 1 in nums: continue
            t = x
            while t in nums: t -= 1
            ans = max(ans, x - t)
        return ans
</code></pre>
]]></content>
    <author><name>Hygen</name></author>
    <category term="hot100"/>
  </entry>
  <entry>
    <title>删除链表的中间节点</title>
    <link href="https://www.hygen.red/posts/dsa/solution/005%E5%88%A0%E9%99%A4%E9%93%BE%E8%A1%A8%E7%9A%84%E4%B8%AD%E9%97%B4%E8%8A%82%E7%82%B9/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/dsa/solution/005%E5%88%A0%E9%99%A4%E9%93%BE%E8%A1%A8%E7%9A%84%E4%B8%AD%E9%97%B4%E8%8A%82%E7%82%B9/</id>
    <published>2026-06-15T00:00:00.000Z</published>
    <updated>2026-06-15T00:00:00.000Z</updated>
    <summary>LeetCode-2095</summary>
    <content type="html"><![CDATA[<h1>题目描述</h1>
<p><a href="https://leetcode.cn/problems/delete-the-middle-node-of-a-linked-list/description/?envType=daily-question&amp;envId=2026-06-15">题目链接</a></p>
<p>给你一个链表的头节点 <code>head</code>，<strong>删除</strong>链表的<strong>中间节点</strong>，并返回修改后的链表的头节点 <code>head</code>。</p>
<p>长度为 <code>n</code> 链表的中间节点是从头数起第 <code>⌊n / 2⌋</code> 个节点（下标从 <strong>0</strong> 开始），其中 <code>⌊x⌋</code> 表示小于或等于 <code>x</code> 的最大整数。</p>
<ul>
<li>对于 <code>n</code> = <code>1</code>、<code>2</code>、<code>3</code>、<code>4</code> 和 <code>5</code> 的情况，中间节点的下标分别是 <code>0</code>、<code>1</code>、<code>1</code>、<code>2</code> 和 <code>2</code>。</li>
</ul>
<h2>约束条件</h2>
<ol>
<li>链表中节点的数目在范围 <code>[1, 10^5]</code> 内</li>
<li><code>1 &lt;= Node.val &lt;= 10^5</code></li>
</ol>
<h2>示例1</h2>
<pre><code>input: head = [1,3,4,7,1,2,6]
output: [1,3,4,1,2,6]
</code></pre>
<p>解释：n = 7，下标为 3 的节点（值为 7）是中间节点，删除后返回新链表。</p>
<h2>示例2</h2>
<pre><code>input: head = [1,2,3,4]
output: [1,2,4]
</code></pre>
<p>解释：n = 4，下标为 2 的节点（值为 3）是中间节点。</p>
<h2>示例3</h2>
<pre><code>input: head = [2,1]
output: [2]
</code></pre>
<p>解释：n = 2，下标为 1 的节点（值为 1）是中间节点，删除后只剩节点 0。</p>
<h1>解题思路</h1>
<p>这题和 <a href="https://leetcode.cn/problems/middle-of-the-linked-list/">876. 链表的中间结点</a> 很像，区别在于那题只需要<strong>找到</strong>中间节点，而这题需要<strong>删除</strong>它。删除操作需要拿到中间节点的前驱，因此不能直接把慢指针停在中间节点上。</p>
<h2>两次遍历</h2>
<p>最直观的做法：先遍历一遍统计链表长度 <code>n</code>，再从头走到第 <code>⌊n / 2⌋</code> 个节点的前一个位置，执行 <code>prev.next = prev.next.next</code> 即可。为了统一处理删除头节点（<code>n = 1</code>）的情况，可以在链表头部加一个哑节点 <code>dummy</code>：</p>
<pre><code>class Solution:
    def deleteMiddle(self, head: Optional[ListNode]) -&gt; Optional[ListNode]:
        n = 0
        cur = head
        while cur:
            n += 1
            cur = cur.next
        dummy = ListNode(0, head)
        cur = dummy
        for _ in range(n // 2):
            cur = cur.next
        cur.next = cur.next.next
        return dummy.next
</code></pre>
<p>时间复杂度 $O(n)$，空间复杂度 $O(1)$，但需要遍历链表两次。</p>
<h2>快慢指针</h2>
<p>能否只遍历一次？可以。快慢指针的经典结论是：快指针每次走两步、慢指针每次走一步，当快指针到达末尾时，慢指针恰好停在中间节点。但删除需要的是<strong>中间节点的前驱</strong>，因此让慢指针比快指针<strong>晚出发一步</strong>——具体做法是用 <code>dummy</code> 节点作为慢指针起点，快指针从 <code>head</code> 出发，循环条件为 <code>fast.next and fast.next.next</code>：</p>
<pre><code>dummy -&gt; 1 -&gt; 3 -&gt; 4 -&gt; 7 -&gt; 1 -&gt; 2 -&gt; 6
slow^   fast^
</code></pre>
<p>快指针每次跳两步，慢指针每次跟一步。当 <code>fast.next.next</code> 不存在时停止，此时 <code>slow</code> 指向中间节点的前驱，直接跳过下一个节点即可完成删除。</p>
<p>以 <code>n = 7</code> 为例，三轮循环后 <code>slow</code> 停在值为 4 的节点，<code>slow.next</code> 即为待删的中间节点 7。</p>
<h2>时间复杂度</h2>
<p>$O(n)$，只需遍历链表一次；空间复杂度 $O(1)$。</p>
<h2>代码</h2>
<pre><code># Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def deleteMiddle(self, head: Optional[ListNode]) -&gt; Optional[ListNode]:
        dummy = ListNode(0, head)
        slow = fast = dummy
        while fast.next and fast.next.next:
            slow = slow.next
            fast = fast.next.next
        slow.next = slow.next.next
        return dummy.next
</code></pre>
]]></content>
    <author><name>Hygen</name></author>
    <category term="Algorithm and Structure"/>
  </entry>
  <entry>
    <title>字母异位词分组</title>
    <link href="https://www.hygen.red/posts/hot100/01-%E5%93%88%E5%B8%8C/002%E5%AD%97%E6%AF%8D%E5%BC%82%E4%BD%8D%E8%AF%8D%E5%88%86%E7%BB%84/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/hot100/01-%E5%93%88%E5%B8%8C/002%E5%AD%97%E6%AF%8D%E5%BC%82%E4%BD%8D%E8%AF%8D%E5%88%86%E7%BB%84/</id>
    <published>2026-06-15T00:00:00.000Z</published>
    <updated>2026-06-15T00:00:00.000Z</updated>
    <summary>Leetcode-49</summary>
    <content type="html"><![CDATA[<h1>题目描述</h1>
<p><a href="https://leetcode.cn/problems/group-anagrams/description/?envType=study-plan-v2&amp;envId=top-100-liked">题目链接</a></p>
<h2>输入</h2>
<p>一个字符串数组 <code>strs</code></p>
<h2>输出</h2>
<p>将字母异位词组合在一起，按任意顺序返回结果列表</p>
<h2>约束条件</h2>
<ol>
<li><code>1 &lt;= strs.length &lt;= 10^4</code></li>
<li><code>0 &lt;= strs[i].length &lt;= 100</code></li>
<li><code>strs[i]</code> 仅包含小写字母</li>
</ol>
<h2>示例1</h2>
<pre><code>input: strs = ["eat","tea","tan","ate","nat","bat"]
output: [["bat"],["nat","tan"],["ate","eat","tea"]]
</code></pre>
<p>解释：<code>"nat"</code> 和 <code>"tan"</code> 互为字母异位词；<code>"ate"</code>、<code>"eat"</code>、<code>"tea"</code> 互为字母异位词；<code>"bat"</code> 单独成组。</p>
<h2>示例2</h2>
<pre><code>input: strs = [""]
output: [[""]]
</code></pre>
<h2>示例3</h2>
<pre><code>input: strs = ["a"]
output: [["a"]]
</code></pre>
<h1>解题思路</h1>
<p>题目要求把<strong>字母异位词</strong>分到同一组。所谓字母异位词，就是两个字符串包含的字符种类和数量完全相同，只是排列顺序不同，比如 <code>"eat"</code> 和 <code>"tea"</code>。</p>
<p>很容易想到对每个字符串去和其他字符串逐一比较，判断是否为异位词，相同则划入同一组。但外层要对 <code>n</code> 个字符串两两配对，整体复杂度约为 $O(n^2 \cdot k)$（<code>k</code> 为字符串最大长度），在 <code>strs.length</code> 达到 $10^4$ 时不够快，此路不通。</p>
<p>不过我们依旧可以想想：判断两个字符串是否为异位词，有一个很简单的办法——排序后比较。<code>"eat"</code> 和 <code>"tea"</code> 排序后都是 <code>"aet"</code>，而 <code>"bat"</code> 排序后是 <code>"abt"</code>，彼此不同。这就给了我们一个思路：我们并不需要真的去两两"比较"，只需要为每个字符串生成一个<strong>分组标识</strong>，凡是异位词，标识必须相同；非异位词，标识必须不同。</p>
<p>什么可以作为分组标识？既然异位词排序后结果一样，把每个字符串排序，再拼回字符串，就可以当作 key：</p>
<pre><code>"eat" -&gt; "aet"
"tea" -&gt; "aet"
"tan" -&gt; "ant"
</code></pre>
<p>接下来就是把字符串按标识归类。什么结构适合"按某个 key 分组存放"呢？显然<strong>哈希表</strong>可以，且它的存取都是 $O(1)$ 的。这题每个 key 对应的是一组字符串，value 需要是一个列表，直接用 python 中的 <code>defaultdict(list)</code> 即可——遇到新 key 时自动创建空列表，省去手动判断 key 是否存在的步骤。</p>
<p>遍历 <code>strs</code> 时，对当前字符串 <code>s</code> 计算 <code>''.join(sorted(s))</code> 作为 key，把 <code>s</code> 本身 <code>append</code> 进 <code>d[key]</code>。注意存的是<strong>原字符串</strong>，不是排序后的结果，题目要返回的是分组后的原始字符串。</p>
<p>全部遍历结束后，哈希表里每个 value 就是一组异位词。题目要求返回 <code>List[List[str]]</code>，而 <code>d.values()</code> 拿到的是字典值的视图，用列表推导 <code>[v for v in d.values()]</code> 收集成列表返回即可。</p>
<p>时间复杂度 $O(n \cdot k \log k)$，其中 <code>n</code> 为字符串个数，<code>k</code> 为字符串最大长度（排序开销）；空间复杂度 $O(n \cdot k)$。</p>
<h2>代码</h2>
<pre><code>class Solution:
    def groupAnagrams(self, strs: List[str]) -&gt; List[List[str]]:
        d = defaultdict(list)
        for s in strs:
            d[''.join(sorted(s))].append(s)
        return [v for v in d.values()]
</code></pre>
]]></content>
    <author><name>Hygen</name></author>
    <category term="hot100"/>
  </entry>
  <entry>
    <title>链表最大孪生和</title>
    <link href="https://www.hygen.red/posts/dsa/solution/004%E9%93%BE%E8%A1%A8%E6%9C%80%E5%A4%A7%E5%AD%AA%E7%94%9F%E5%92%8C/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/dsa/solution/004%E9%93%BE%E8%A1%A8%E6%9C%80%E5%A4%A7%E5%AD%AA%E7%94%9F%E5%92%8C/</id>
    <published>2026-06-14T00:00:00.000Z</published>
    <updated>2026-06-14T00:00:00.000Z</updated>
    <summary>LeetCode-2130</summary>
    <content type="html"><![CDATA[<h1>题目描述</h1>
<p><a href="https://leetcode.cn/problems/maximum-twin-sum-of-a-linked-list/description/?envType=daily-question&amp;envId=2026-06-14">题目链接</a></p>
<p>在一个大小为<code>n</code>且<code>n</code>为偶数的链表中，第1个节点和第n个节点为一对孪生节点，第2对与第n-1对为一对孪生节点，依此类推...</p>
<p><strong>孪生和</strong>为一个节点和它的孪生节点两者之和</p>
<p>给一个长度为<code>n</code>的链表<code>head</code>，要求返回链表的最大孪生和</p>
<h2>示例1</h2>
<pre><code>input: head=[5， 4， 2， 1]
output: 6
</code></pre>
<p>解释：5 + 1 = 6， 4 + 2 = 6，最大孪生和为6</p>
<h2>示例2</h2>
<pre><code>input: head=[4， 2， 2， 3]
output: 7
</code></pre>
<p>解释： 4 + 3 = 7</p>
<h1>解题思路</h1>
<p>如果是在数组上做这道题，那么题目将会非常简单，只需要按顺序求arr[0] + arr[n-1], ... , arr[n // 2 - 1] + arr[n // 2]的最大值即可，但是这题是在链表中，无法随机存取，因此想要从后往前推需要办一些手续。</p>
<p>我们正常遍历链表一定是从左往右，那么如何能让它遍历的顺序从右往左呢？仅需要考虑遍历的话，我们可以使用<code>DFS</code>，因为它从左往右走完一遍后必定还要从右往左回来一趟，我们可以考虑仅利用它的后半程，在此期间再运用一个从左往右遍历的指针即可计算孪生和并取最大值。</p>
<h2>时间复杂度</h2>
<p>$O(n)$，n为链表元素个数</p>
<h2>代码</h2>
<pre><code># Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def pairSum(self, head: Optional[ListNode]) -&gt; int:
        ans = 0
        l = head

        def dfs(r: Optional[ListNode]):
            if r.next: dfs(r.next)
            nonlocal ans, l
            ans = max(ans, l.val + r.val)
            l = l.next
        
        dfs(head)

        return ans
</code></pre>
]]></content>
    <author><name>Hygen</name></author>
    <category term="Algorithm and Structure"/>
  </entry>
  <entry>
    <title>两数之和</title>
    <link href="https://www.hygen.red/posts/hot100/01-%E5%93%88%E5%B8%8C/001%E4%B8%A4%E6%95%B0%E4%B9%8B%E5%92%8C/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/hot100/01-%E5%93%88%E5%B8%8C/001%E4%B8%A4%E6%95%B0%E4%B9%8B%E5%92%8C/</id>
    <published>2026-06-14T00:00:00.000Z</published>
    <updated>2026-06-14T00:00:00.000Z</updated>
    <summary>Leetcode-1</summary>
    <content type="html"><![CDATA[<h1>题目描述</h1>
<p><a href="https://leetcode.cn/problems/two-sum/?envType=study-plan-v2&amp;envId=top-100-liked">题目链接</a></p>
<h2>输入</h2>
<p>一个整数数组<code>nums</code></p>
<p>一个整数目标值<code>target</code></p>
<h2>输出</h2>
<p>两个数组下标，数组中这两个下标的值的和为<code>target</code></p>
<h2>约束条件</h2>
<ol>
<li>不能使用两次相同的下标</li>
<li>可以按任意顺序返回答案</li>
<li>只存在一个有效答案</li>
</ol>
<h1>进阶约束</h1>
<ol>
<li>时间复杂度需要小于$O(n^2)$</li>
</ol>
<h2>示例1</h2>
<pre><code>input: nums = [2,7,11,15], target = 9
output: [0,1]
</code></pre>
<p>解释： nums[0] = 2, nums[1] = 7, 2 + 7 = 9</p>
<h2>示例2</h2>
<pre><code>input: nums = [3,2,4], target = 6
output: [1,2]
</code></pre>
<h2>示例3</h2>
<pre><code>input: nums = [3,3], target = 6
output: [0,1]
</code></pre>
<h1>解题思路</h1>
<p>题目很好理解，从数组中找出两个值，和为<code>target</code>即可，然后返回这两个值的下标就行。</p>
<p>很容易想到使用双循环来完成这题，没什么思考难度，不多赘述。但是进阶约束中需要我们的时间复杂度小于$O(n^2)$，这显然就是为了卡双循环，因此此路不通。不过我们依旧可以先将这个暴力解法的代码写出来，看看能不能优化：</p>
<pre><code>class Solution:
    def twoSum(self, nums: List[int], target: int) -&gt; List[int]:
        n = len(nums)
        for i in range(n - 1):
            for j in range(i + 1, n):
                if nums[i] + nums[j] == target:
                    return [i, j]
</code></pre>
<p>从暴力的第二层循环来看，它并不在乎以前的值，因为以前的值在第一层循环中已经经历过了，比如当<code>i = 2, j = 3</code>时，<code>i = 1</code>的情况在第一层循环中已经结束了，因此返过来，对于第二层循环来说，都是每个<code>nums[j]</code>值在前面已经遍历过的数中找和为<code>target</code>的配对，从这个角度上看，第一层循环就成为了非必要的循环，只要我们能找到一种方法，能够直接将以前遍历过的数存起来就行了。</p>
<p>什么方法能直接存以前遍历过的数值呢？显然<code>哈希表</code>是可以的，且它的存取都是$O(1)$的，这不需要自己动手造轮子，直接使用python中的<code>defaultdict</code>类即可。</p>
<h2>代码</h2>
<pre><code>class Solution:
    def twoSum(self, nums: List[int], target: int) -&gt; List[int]:
        hh = defaultdict(int)
        for i, x in enumerate(nums):
            if target - x in hh: return [hh[target - x], i]
            hh[x] = i
</code></pre>
]]></content>
    <author><name>Hygen</name></author>
    <category term="hot100"/>
  </entry>
  <entry>
    <title>AI应用-实战</title>
    <link href="https://www.hygen.red/posts/pylearning/002_ai%E5%BA%94%E7%94%A8/001ai%E5%BA%94%E7%94%A8%E5%AE%9E%E6%88%98/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/pylearning/002_ai%E5%BA%94%E7%94%A8/001ai%E5%BA%94%E7%94%A8%E5%AE%9E%E6%88%98/</id>
    <published>2026-05-26T00:00:00.000Z</published>
    <updated>2026-05-26T00:00:00.000Z</updated>
    <summary>基于Streamlit与DeepSeek实现AI智能伴侣</summary>
    <content type="html"><![CDATA[<h1>1. Streamlit</h1>
<h2>1.1. Streamlit入门</h2>
<p><a href="https://streamlit.io">Streamlit</a> 是一个开源 Python 框架，可以用很少的代码快速把数据分析、机器学习模型或脚本做成交互式 Web 应用，无需掌握前端基础。</p>
<h3>1.1.1. Streamlit入门程序步骤</h3>
<ol>
<li>安装streamlit：<code>pip install streamlit</code></li>
<li>在python中引入streamlit模块：<code>import streamlit as st</code></li>
<li>基于streamlit中提供的API来构建Web应用</li>
<li>终端中运行程序：<code>streamlit run xxx.py</code></li>
</ol>
<h3>1.1.2. 入门程序示例</h3>
<pre><code>import streamlit as st

st.title("我的第一个Streamlit应用")
st.header("这是一个header")
st.subheader("这是一个subheader")
</code></pre>
<p>运行后会在浏览器打开如下页面：
<img src="./streamlit%E5%85%A5%E9%97%A8.png" alt="streamlit入门" /></p>
]]></content>
    <author><name>Hygen</name></author>
  </entry>
  <entry>
    <title>Python核心语法03-数据存储容器</title>
    <link href="https://www.hygen.red/posts/pylearning/001_python%E6%A0%B8%E5%BF%83%E8%AF%AD%E6%B3%95/003%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8%E5%AE%B9%E5%99%A8/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/pylearning/001_python%E6%A0%B8%E5%BF%83%E8%AF%AD%E6%B3%95/003%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8%E5%AE%B9%E5%99%A8/</id>
    <published>2026-05-24T00:00:00.000Z</published>
    <updated>2026-05-24T00:00:00.000Z</updated>
    <summary>Python数据存储容器学习</summary>
    <content type="html"><![CDATA[<h1>1. 数据容器概述</h1>
<h2>1.1. 什么是数据容器</h2>
<p>数据容器是一种可以容纳多份数据的数据类型，容纳的每一份数据称之为1个元素，每一个元素都可以是任意类型的数据，如：字符串、数字、布尔等。</p>
<p>在Python中，主要有以下5种数据容器：</p>
<ul>
<li>字符串（str）</li>
<li>列表（list）</li>
<li>元组（tuple）</li>
<li>集合（set）</li>
<li>字典（dict）</li>
</ul>
<h2>1.2. 为什么需要数据容器</h2>
<p>当我们需要存储多个相关数据时，如果使用单个变量会非常繁琐：</p>
<pre><code># 单个变量存储多个成绩
score1 = 695
score2 = 558
score3 = 622
score4 = 589
score5 = 645
# ... 更多变量
</code></pre>
<p>使用数据容器可以让代码更加简洁优雅：</p>
<pre><code># 列表存储多个成绩
score_list = [695, 558, 622, 589, 645, 607, 577, 552, 475, 602]
</code></pre>
<h2>1.3. 数据容器特性对比</h2>
<table>
<thead>
<tr>
<th>特性</th>
<th>字符串（str）</th>
<th>列表（list）</th>
<th>元组（tuple）</th>
<th>集合（set）</th>
<th>字典（dict）</th>
</tr>
</thead>
<tbody>
<tr>
<td>有序性</td>
<td>有序</td>
<td>有序</td>
<td>有序</td>
<td>无序</td>
<td>有序(3.7+)</td>
</tr>
<tr>
<td>重复元素</td>
<td>允许</td>
<td>允许</td>
<td>允许</td>
<td>不允许</td>
<td>key不允许</td>
</tr>
<tr>
<td>可变性</td>
<td>不可变</td>
<td>可变</td>
<td>不可变</td>
<td>可变</td>
<td>可变</td>
</tr>
<tr>
<td>索引访问</td>
<td>支持</td>
<td>支持</td>
<td>支持</td>
<td>不支持</td>
<td>不支持</td>
</tr>
<tr>
<td>切片操作</td>
<td>支持</td>
<td>支持</td>
<td>支持</td>
<td>不支持</td>
<td>不支持</td>
</tr>
<tr>
<td>使用场景</td>
<td>文本处理</td>
<td>有序可重复数据集合</td>
<td>固定数据记录</td>
<td>去重数据集合</td>
<td>键值对</td>
</tr>
</tbody>
</table>
<h1>2. 列表（list）</h1>
<h2>2.1. 列表基础</h2>
<h3>2.1.1. 列表介绍</h3>
<p>列表是数据容器中的一类，是一次性可以存储多个数据（元素）的。</p>
<p><strong>特点：</strong></p>
<ul>
<li>可以存储不同类型的元素</li>
<li>元素有序</li>
<li>元素可以重复</li>
<li>元素可以修改</li>
</ul>
<h3>2.1.2. 列表定义</h3>
<pre><code># 定义列表
列表名称 = [元素1, 元素2, 元素3, 元素4, 元素5...]

# 示例
score_list = [695, 558, 622, 589, 645]
mixed_list = [100, "Python", True, 3.14]
</code></pre>
<h3>2.1.3. 列表索引</h3>
<p>列表中的每一个元素都有其对应的下标（索引），通过元素对应的索引，就可以获取到对应的元素。</p>
<ul>
<li>从前向后（正向索引），下标从0开始</li>
<li>从后向前（反向索引），下标从-1开始</li>
</ul>
<pre><code>s = [54, 15, 75, 108, 23, 78, 75]

# 正向索引
print(s[0])  # 输出: 54
print(s[1])  # 输出: 15
print(s[6])  # 输出: 75

# 反向索引
print(s[-1])  # 输出: 75
print(s[-2])  # 输出: 78
print(s[-7])  # 输出: 54
</code></pre>
<p><strong>注意：</strong> 如果指定的索引值超出范围，将会报错。</p>
<h3>2.1.4. 列表元素的基本操作</h3>
<pre><code># 查看元素
print(score_list[0])

# 修改元素
score_list[0] = 700
print(score_list)  # 输出: [700, 558, 622, 589, 645]

# 删除元素
del score_list[3]
print(score_list)  # 输出: [700, 558, 622, 645]
</code></pre>
<h2>2.2. 列表切片</h2>
<h3>2.2.1. 切片介绍</h3>
<p>切片是指对操作的数据截取其中一部分的操作。列表、字符串、元组都支持切片操作。</p>
<h3>2.2.2. 切片语法</h3>
<pre><code>序列数据[开始索引:结束索引:步长]
</code></pre>
<p><strong>特点：</strong></p>
<ul>
<li>不包含结束索引位置对应的元素</li>
<li>开始索引未指定默认为0</li>
<li>结束索引未指定默认为列表长度（直到列表末尾）</li>
<li>步长未指定默认为1</li>
<li>索引采用正向、反向索引都可以</li>
<li>步长是选取间隔</li>
</ul>
<h3>2.2.3. 切片示例</h3>
<pre><code>s = ["A", "B", "C", "E", "D", "E", "G"]

# 基本切片
print(s[0:5:1])  # 输出: ["A", "B", "C", "E", "D"]
print(s[0:5:2])  # 输出: ["A", "C", "D"]

# 省略步长
print(s[0:5])  # 输出: ["A", "B", "C", "E", "D"]

# 省略开始索引
print(s[:5])  # 输出: ["A", "B", "C", "E", "D"]

# 省略结束索引
print(s[2:])  # 输出: ["C", "E", "D", "E", "G"]

# 反向切片
print(s[::-1])  # 输出: ["G", "E", "D", "E", "C", "B", "A"]
</code></pre>
<h2>2.3. 列表常用方法</h2>
<p>列表的常用方法就是指列表这种数据类型内置的常见功能（添加元素、删除元素、排序等）。</p>
<table>
<thead>
<tr>
<th>方法</th>
<th>作用</th>
<th>样例</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>append()</code></td>
<td>在列表的尾部追加元素</td>
<td><code>s.append(10086)</code></td>
</tr>
<tr>
<td><code>insert()</code></td>
<td>在指定索引之前，插入该元素</td>
<td><code>s.insert(0, 92)</code></td>
</tr>
<tr>
<td><code>remove()</code></td>
<td>移除列表中第一个匹配到的值</td>
<td><code>s.remove(75)</code></td>
</tr>
<tr>
<td><code>pop()</code></td>
<td>删除列表中指定索引位置的元素（如果未指定索引，默认删最后一个）</td>
<td><code>s.pop(2)</code> / <code>s.pop()</code></td>
</tr>
<tr>
<td><code>sort()</code></td>
<td>对列表进行排序（列表元素的数据类型一致，才可以进行排序）</td>
<td><code>s.sort()</code></td>
</tr>
<tr>
<td><code>reverse()</code></td>
<td>反转列表元素</td>
<td><code>s.reverse()</code></td>
</tr>
</tbody>
</table>
<h3>2.3.1. 添加元素</h3>
<pre><code># 尾部追加元素
score_list = [695, 558, 622]
score_list.append(589)
print(score_list)  # 输出: [695, 558, 622, 589]

# 指定位置插入元素
score_list.insert(1, 645)
print(score_list)  # 输出: [695, 645, 558, 622, 589]
</code></pre>
<h3>2.3.2. 删除元素</h3>
<pre><code># 删除指定值的第一个元素
score_list = [695, 558, 622, 558, 645]
score_list.remove(558)
print(score_list)  # 输出: [695, 622, 558, 645]

# 删除指定索引的元素
score_list.pop(2)
print(score_list)  # 输出: [695, 622, 645]

# 删除最后一个元素
score_list.pop()
print(score_list)  # 输出: [695, 622]
</code></pre>
<h3>2.3.3. 排序与反转</h3>
<pre><code># 排序
num_list = [54, 15, 75, 108, 23, 78]
num_list.sort()
print(num_list)  # 输出: [15, 23, 54, 75, 78, 108]

# 降序排序
num_list.sort(reverse=True)
print(num_list)  # 输出: [108, 78, 75, 54, 23, 15]

# 反转列表
num_list.reverse()
print(num_list)  # 输出: [15, 23, 54, 75, 78, 108]
</code></pre>
<h2>2.4. 列表常用操作</h2>
<h3>2.4.1. 列表合并</h3>
<pre><code># 使用+运算符合并
num_list1 = [19, 23, 54, 64, 875]
num_list2 = [55, 80, 72, 35, 60]
merged_list = num_list1 + num_list2
print(merged_list)  # 输出: [19, 23, 54, 64, 875, 55, 80, 72, 35, 60]

# 使用*解包操作
merged_list = [*num_list1, *num_list2]
print(merged_list)  # 输出: [19, 23, 54, 64, 875, 55, 80, 72, 35, 60]
</code></pre>
<h3>2.4.2. 元素存在性判断</h3>
<pre><code>num_list = [19, 23, 54, 64, 875]
print(23 in num_list)  # 输出: True
print(100 in num_list)  # 输出: False
print(100 not in num_list)  # 输出: True
</code></pre>
<h3>2.4.3. 统计函数</h3>
<p>Python中提供了一些用于数据统计的内置函数：</p>
<table>
<thead>
<tr>
<th>函数</th>
<th>作用</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>min()</code></td>
<td>获取最小值</td>
</tr>
<tr>
<td><code>max()</code></td>
<td>获取最大值</td>
</tr>
<tr>
<td><code>sum()</code></td>
<td>求和</td>
</tr>
<tr>
<td><code>len()</code></td>
<td>获取元素的个数</td>
</tr>
</tbody>
</table>
<pre><code>score_list = [695, 558, 622, 589, 645]
print(min(score_list))  # 输出: 558
print(max(score_list))  # 输出: 695
print(sum(score_list))  # 输出: 3109
print(len(score_list))  # 输出: 5
</code></pre>
<h2>2.5. 列表推导式</h2>
<h3>2.5.1. 什么是列表推导式</h3>
<p>列表推导式就是按照一定规则快速生成一个列表的方法。</p>
<h3>2.5.2. 列表推导式语法</h3>
<pre><code># 格式1：基本格式
列表名称 = [要插入列表的数据 for i in 列表]

# 格式2：带条件格式
列表名称 = [要插入列表的数据 for i in 列表 if 条件]
</code></pre>
<h3>2.5.3. 列表推导式示例</h3>
<pre><code># 生成1-20的平方列表
square_list = [i ** 2 for i in range(1, 21)]
print(square_list)

# 从数字列表中提取所有偶数，并计算其平方
num_list = [19, 23, 54, 64, 87, 20, 109, 232, 123, 43, 26, 55, 72]
even_square_list = [i ** 2 for i in num_list if i % 2 == 0]
print(even_square_list)
</code></pre>
<h2>2.6. 案例实操</h2>
<h3>2.6.1. 案例1：成绩统计</h3>
<p>需求：将用户输入的10个数字，存储到一个列表中，并将列表中的数字进行排序，输出其中的最小值、最大值和平均值。</p>
<pre><code># 存储用户输入的成绩
scores = []

# 循环输入10个成绩
for i in range(10):
    score = int(input(f"请输入第{i+1}个成绩: "))
    scores.append(score)

# 排序
scores.sort()
print("排序后的成绩:", scores)

# 计算统计值
min_score = min(scores)
max_score = max(scores)
avg_score = sum(scores) / len(scores)

print(f"最小值: {min_score}")
print(f"最大值: {max_score}")
print(f"平均值: {avg_score:.2f}")
</code></pre>
<h3>2.6.2. 案例2：列表合并与去重</h3>
<p>需求：合并两个列表中的元素，并对合并的结果进行去重处理。</p>
<pre><code># 定义列表
num_list1 = [19, 23, 54, 64, 875, 20, 109, 232, 123, 54]
num_list2 = [55, 80, 72, 35, 60, 123, 54, 29, 91]

# 合并列表
merged_list = num_list1 + num_list2

# 去重
unique_list = []
for num in merged_list:
    if num not in unique_list:
        unique_list.append(num)

print("合并后的列表:", merged_list)
print("去重后的列表:", unique_list)
</code></pre>
<h1>3. 字符串（str）</h1>
<h2>3.1. 字符串基础</h2>
<h3>3.1.1. 字符串介绍</h3>
<p>字符串是字符的容器，一个字符串中可以存放任意数量的字符。</p>
<p><strong>特点：</strong></p>
<ul>
<li>不可变性（无法修改）</li>
<li>有序性</li>
<li>可迭代性</li>
</ul>
<h3>3.1.2. 字符串定义</h3>
<pre><code># 单引号定义
s1 = 'Python'

# 双引号定义
s2 = "Python"

# 三引号定义（支持多行）
s3 = """Python
是一门
编程语言"""
</code></pre>
<h3>3.1.3. 字符串索引</h3>
<p>字符串中的每一个字符元素都有其对应的下标（索引），通过元素对应的索引，就可以获取到对应的元素。</p>
<pre><code>s = "Python"

# 正向索引
print(s[0])  # 输出: P
print(s[1])  # 输出: y
print(s[5])  # 输出: n

# 反向索引
print(s[-1])  # 输出: n
print(s[-2])  # 输出: o
print(s[-6])  # 输出: P
</code></pre>
<h2>3.2. 字符串切片</h2>
<p>字符串也支持切片操作，语法与列表切片相同。</p>
<pre><code>s = "Python"

# 基本切片
print(s[0:5:1])  # 输出: Pytho
print(s[0:5:2])  # 输出: Pto

# 反向切片
print(s[::-1])  # 输出: nohtyP
</code></pre>
<h2>3.3. 字符串常用方法</h2>
<table>
<thead>
<tr>
<th>方法</th>
<th>作用</th>
<th>样例</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>find()</code></td>
<td>在字符串中查找子串，返回第一次出现的索引位置，找不到返回-1</td>
<td><code>s.find('Python')</code></td>
</tr>
<tr>
<td><code>count()</code></td>
<td>统计子串在字符串中出现的次数</td>
<td><code>s.count('H')</code></td>
</tr>
<tr>
<td><code>upper()</code></td>
<td>将字符串中的所有字母转换为大写</td>
<td><code>s.upper()</code></td>
</tr>
<tr>
<td><code>lower()</code></td>
<td>将字符串中的所有字母转换为小写</td>
<td><code>s.lower()</code></td>
</tr>
<tr>
<td><code>split()</code></td>
<td>将字符串按指定分隔符分割成列表</td>
<td><code>s.split(' ')</code></td>
</tr>
<tr>
<td><code>strip()</code></td>
<td>去除字符串两端的空白字符或指定字符</td>
<td><code>s.strip()</code> / <code>s.strip('*')</code></td>
</tr>
<tr>
<td><code>replace()</code></td>
<td>将字符串中的指定子串替换为新的子串</td>
<td><code>s.replace('H','C')</code></td>
</tr>
<tr>
<td><code>startswith()</code></td>
<td>检查字符串是否以指定子串开头，返回布尔值</td>
<td><code>s.startswith('P')</code></td>
</tr>
</tbody>
</table>
<h3>3.3.1. 查找与统计</h3>
<pre><code>s = "Hello World, Hello Python"

# 查找子串
print(s.find("Hello"))  # 输出: 0
print(s.find("Python"))  # 输出: 13
print(s.find("Java"))  # 输出: -1

# 统计子串出现次数
print(s.count("Hello"))  # 输出: 2
print(s.count("o"))  # 输出: 4
</code></pre>
<h3>3.3.2. 大小写转换</h3>
<pre><code>s = "Hello World"

# 转换为大写
print(s.upper())  # 输出: HELLO WORLD

# 转换为小写
print(s.lower())  # 输出: hello world
</code></pre>
<h3>3.3.3. 分割与连接</h3>
<pre><code># 分割字符串
s = "apple,banana,orange"
fruit_list = s.split(",")
print(fruit_list)  # 输出: ['apple', 'banana', 'orange']

# 连接字符串
fruit_str = "-".join(fruit_list)
print(fruit_str)  # 输出: apple-banana-orange
</code></pre>
<h3>3.3.4. 去除空白字符</h3>
<pre><code>s = "   Hello World   "
print(s.strip())  # 输出: Hello World
print(s.lstrip())  # 输出: Hello World   
print(s.rstrip())  # 输出:    Hello World
</code></pre>
<h3>3.3.5. 替换子串</h3>
<pre><code>s = "Hello World"
new_s = s.replace("World", "Python")
print(new_s)  # 输出: Hello Python
</code></pre>
<h2>3.4. 字符串常用操作</h2>
<h3>3.4.1. 子串存在性判断</h3>
<pre><code>s = "Hello World"
print("Hello" in s)  # 输出: True
print("Python" in s)  # 输出: False
</code></pre>
<h3>3.4.2. 字符串拼接</h3>
<pre><code># 使用+运算符拼接
s1 = "Hello"
s2 = "World"
s3 = s1 + " " + s2
print(s3)  # 输出: Hello World

# 使用f-string格式化
name = "Tom"
age = 18
s = f"我叫{name}，今年{age}岁"
print(s)  # 输出: 我叫Tom，今年18岁
</code></pre>
<h2>3.5. 案例实操</h2>
<h3>3.5.1. 案例1：邮箱格式验证</h3>
<p>需求：用户输入一个邮箱，验证邮箱格式是否正确（包含一个@和至少一个.），如果输入正确，输出"邮箱格式正确"，否则输出"邮箱格式错误"。</p>
<pre><code>email = input("请输入邮箱地址: ")

if "@" in email and "." in email:
    print("邮箱格式正确")
else:
    print("邮箱格式错误")
</code></pre>
<h3>3.5.2. 案例2：判断回文串</h3>
<p>需求：输入一个字符串，判断该字符串是否是回文（两边对称）。</p>
<pre><code>s = input("请输入一个字符串: ")

# 去除空格并转换为小写
s = s.replace(" ", "").lower()

# 判断是否是回文
if s == s[::-1]:
    print("是回文串")
else:
    print("不是回文串")
</code></pre>
<h1>4. 元组（tuple）</h1>
<h2>4.1. 元组基础</h2>
<h3>4.1.1. 元组介绍</h3>
<p>元组是不可变的序列，类似于列表，但创建后不能修改。</p>
<p><strong>特点：</strong></p>
<ul>
<li>可以存储不同类型的元素</li>
<li>元素可以重复</li>
<li>有序</li>
<li>不可以修改（只读）</li>
<li>支持索引访问和切片</li>
</ul>
<h3>4.1.2. 元组定义</h3>
<pre><code># 定义元组
元组名称 = (元素1, 元素2, ...)

# 定义空元组
元组名称 = ()
元组名称 = tuple()

# 示例
t1 = (5, 7, 9, 1, 2, 3)
t2 = ("Python", 3.14, True)
t3 = ()
t4 = tuple()
</code></pre>
<p><strong>注意：</strong> 定义单元素元组时，需要在结尾加上逗号。</p>
<pre><code># 错误写法（这不是元组，而是一个整数）
t = (5)
print(type(t))  # 输出: &lt;class 'int'&gt;

# 正确写法
t = (5,)
print(type(t))  # 输出: &lt;class 'tuple'&gt;
</code></pre>
<h2>4.2. 元组常用方法</h2>
<p>由于元组是不可变的，所以它的方法主要是查询方法：</p>
<table>
<thead>
<tr>
<th>方法</th>
<th>作用</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>count()</code></td>
<td>统计某元素在元组中出现的次数</td>
</tr>
<tr>
<td><code>index()</code></td>
<td>查找某个元素在元组中的索引位置（第一次出现的位置）</td>
</tr>
</tbody>
</table>
<pre><code>t1 = (5, 7, 9, 1, 2, 3, 10, 6, 4, 8, 12, 7, 5)

# 统计元素出现次数
print(t1.count(7))  # 输出: 2
print(t1.count(5))  # 输出: 2

# 查找元素索引
print(t1.index(9))  # 输出: 2
print(t1.index(12))  # 输出: 10
</code></pre>
<h2>4.3. 组包与解包</h2>
<h3>4.3.1. 什么是组包与解包</h3>
<ul>
<li><strong>组包（Packing）</strong>：将多个值合并到一个容器（元组、列表）中。</li>
<li><strong>解包（Unpacking）</strong>：将容器（元组、列表）解开成独立的元素，分别赋值给多个变量。</li>
</ul>
<h3>4.3.2. 组包示例</h3>
<pre><code># 组包
t1 = (5, 7, 9, 1)
t2 = 5, 7, 9, 1  # 省略括号也是元组

print(type(t1))  # 输出: &lt;class 'tuple'&gt;
print(type(t2))  # 输出: &lt;class 'tuple'&gt;
</code></pre>
<h3>4.3.3. 基础解包</h3>
<pre><code>t1 = (5, 7, 9, 1)

# 基础解包
a, b, c, d = t1
print(a)  # 输出: 5
print(b)  # 输出: 7
print(c)  # 输出: 9
print(d)  # 输出: 1
</code></pre>
<h3>4.3.4. 扩展解包</h3>
<p>在元组解包时，<code>*</code> 表示收集剩余的所有元素，允许我们处理不确定数量的元素（生成列表）。</p>
<pre><code>t2 = (5, 7, 9, 1)

# 扩展解包
x, *y, z = t2
print(x)  # 输出: 5
print(y)  # 输出: [7, 9]
print(z)  # 输出: 1

s, *o = t2
print(s)  # 输出: 5
print(o)  # 输出: [7, 9, 1]

*o, e = t2
print(o)  # 输出: [5, 7, 9]
print(e)  # 输出: 1
</code></pre>
<h2>4.4. 案例实操</h2>
<h3>4.4.1. 案例1：变量交换</h3>
<p>需求：现有两个变量，分别为：a = 10，b = 20，现需要将这两个变量值交换，然后输出到控制台。</p>
<pre><code>a = 10
b = 20

# 传统方法
temp = a
a = b
b = temp
print(a, b)  # 输出: 20 10

# 使用元组解包
a = 10
b = 20
a, b = b, a
print(a, b)  # 输出: 20 10
</code></pre>
<h3>4.4.2. 案例2：多变量交换</h3>
<p>需求：现有三个变量，分别为：a = 100，b = 200，c = 300，现需要将这三个变量值进行交换，将a,b,c的值分别赋值给c,a,b，并将其输出到控制台。</p>
<pre><code>a = 100
b = 200
c = 300

# 使用元组解包
a, b, c = b, c, a
print(a, b, c)  # 输出: 200 300 100
</code></pre>
<h1>5. 集合（set）</h1>
<h2>5.1. 集合基础</h2>
<h3>5.1.1. 集合介绍</h3>
<p>集合（set）是一种无序的、不可重复、可修改的数据容器。</p>
<p><strong>特点：</strong></p>
<ul>
<li>无序</li>
<li>不可重复</li>
<li>可修改</li>
<li>不支持下标索引访问</li>
</ul>
<h3>5.1.2. 集合定义</h3>
<pre><code># 定义集合
s1 = {"C", "D", "X", "T", "O", "U"}

# 定义空集合
s2 = set()

# 错误写法（这是空字典，不是空集合）
s3 = {}
</code></pre>
<p><strong>注意：</strong> 空集合的定义不可以使用<code>{}</code>，<code>{}</code>表示的是空字典。</p>
<h2>5.2. 集合常用操作</h2>
<table>
<thead>
<tr>
<th>操作</th>
<th>含义</th>
<th>样例</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>add(..)</code></td>
<td>添加元素到集合中</td>
<td><code>s1.add('t')</code></td>
</tr>
<tr>
<td><code>remove(..)</code></td>
<td>移除集合中的指定元素（指定元素不存在将报错）</td>
<td><code>s1.remove('t')</code></td>
</tr>
<tr>
<td><code>pop()</code></td>
<td>随机删除集合中的元素并返回</td>
<td><code>e = s1.pop()</code></td>
</tr>
<tr>
<td><code>clear()</code></td>
<td>清空集合</td>
<td><code>s1.clear()</code></td>
</tr>
<tr>
<td><code>difference()</code></td>
<td>求取两个集合的差集（包含在第一个集合但不包含在第二个集合的元素）</td>
<td><code>s1.difference(s2)</code></td>
</tr>
<tr>
<td><code>union()</code></td>
<td>求取两个集合的并集</td>
<td><code>s1.union(s2)</code></td>
</tr>
<tr>
<td><code>intersection()</code></td>
<td>求取两个集合的交集</td>
<td><code>s1.intersection(s2)</code></td>
</tr>
</tbody>
</table>
<h3>5.2.1. 添加元素</h3>
<pre><code>s = {"A", "B", "C"}
s.add("D")
print(s)  # 输出: {'A', 'B', 'C', 'D'}
</code></pre>
<h3>5.2.2. 删除元素</h3>
<pre><code>s = {"A", "B", "C", "D"}

# 删除指定元素
s.remove("B")
print(s)  # 输出: {'A', 'C', 'D'}

# 随机删除一个元素
e = s.pop()
print(e)
print(s)

# 清空集合
s.clear()
print(s)  # 输出: set()
</code></pre>
<h3>5.2.3. 集合运算</h3>
<pre><code>s1 = {1, 2, 3, 4, 5}
s2 = {4, 5, 6, 7, 8}

# 交集
print(s1.intersection(s2))  # 输出: {4, 5}
print(s1 &amp; s2)  # 输出: {4, 5}

# 并集
print(s1.union(s2))  # 输出: {1, 2, 3, 4, 5, 6, 7, 8}
print(s1 | s2)  # 输出: {1, 2, 3, 4, 5, 6, 7, 8}

# 差集
print(s1.difference(s2))  # 输出: {1, 2, 3}
print(s1 - s2)  # 输出: {1, 2, 3}
</code></pre>
<h2>5.3. 集合推导式</h2>
<p>集合也支持推导式写法：</p>
<pre><code># 基本格式
变量名称 = {i表达式 for i in 列表}

# 带条件格式
变量名称 = {i表达式 for i in 列表 if 条件}
</code></pre>
<pre><code># 生成1-10的平方集合
square_set = {i ** 2 for i in range(1, 11)}
print(square_set)

# 从列表中提取所有偶数
num_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
even_set = {i for i in num_list if i % 2 == 0}
print(even_set)
</code></pre>
<h2>5.4. 案例实操</h2>
<h3>5.4.1. 案例1：选课统计</h3>
<p>需求：根据提供的班级学生的选课情况，完成如下需求：</p>
<ol>
<li>找出同时选修了法语和艺术的学生</li>
<li>找出同时选修了所有四门课程的学生</li>
<li>找出选修了足球，但是没有选修篮球的学生</li>
<li>统计每一个学生选修的课程数量</li>
</ol>
<pre><code># 选修足球学生名单
football_set = {"王林", "曾牛", "徐立国", "遁天", "天运子", "韩立", "厉飞雨", "乌丑", "紫灵"}
# 选修篮球学生名单
basketball_set = {"张铁", "墨居仁","王林", "姜老道", "曾牛", "王蝉", "韩立", "天运子", "李化元", "厉飞雨", "云露"}
# 选修法语学生名单
french_set = {"许木", "王卓", "十三", "虎咆", "姜老道", "天运子",  "红蝶", "厉飞雨", "韩立", "曾牛"}
# 选修艺术学生名单
art_set = {"遁天", "天运子", "韩立", "虎咆", "姜老道", "紫灵"}

# 1. 同时选修了法语和艺术的学生
both_french_art = french_set &amp; art_set
print("同时选修了法语和艺术的学生:", both_french_art)

# 2. 同时选修了所有四门课程的学生
all_courses = football_set &amp; basketball_set &amp; french_set &amp; art_set
print("同时选修了所有四门课程的学生:", all_courses)

# 3. 选修了足球，但是没有选修篮球的学生
football_not_basketball = football_set - basketball_set
print("选修了足球，但是没有选修篮球的学生:", football_not_basketball)

# 4. 统计每一个学生选修的课程数量
all_students = football_set | basketball_set | french_set | art_set
for student in all_students:
    count = 0
    if student in football_set:
        count += 1
    if student in basketball_set:
        count += 1
    if student in french_set:
        count += 1
    if student in art_set:
        count += 1
    print(f"{student} 选修了 {count} 门课程")
</code></pre>
<h1>6. 字典（dict）</h1>
<h2>6.1. 字典基础</h2>
<h3>6.1.1. 字典介绍</h3>
<p>Python中的字典（dict），里面存储的是键值对（key: value）类型的数据，可以根据键（key）找到对应的值（value）。</p>
<p><strong>特点：</strong></p>
<ul>
<li>键值对（key: value）存储</li>
<li>键（key）不能重复</li>
<li>可修改</li>
<li>没有索引下标，不能根据索引获取值，只可以根据key获取value</li>
</ul>
<h3>6.1.2. 字典定义</h3>
<pre><code># 定义字典
字典名称 = {key: value, key: value, key: value …}

# 定义空字典
字典名称 = {}
字典名称 = dict()

# 示例
dict1 = {"王林": 670, "韩立": 556, "李慕婉": 582, "紫灵": 435, "许立国": 608}
dict2 = {}
dict3 = dict()
</code></pre>
<p><strong>注意：</strong></p>
<ul>
<li>字典（dict）中的value可以是任何类型的数据</li>
<li>而key不能为可变类型（如：不能为列表list、集合set、字典dict）</li>
<li>字典内的key不允许重复，如果重复定义，后面的覆盖前面的</li>
</ul>
<h3>6.1.3. 根据key获取value</h3>
<pre><code>dict1 = {"王林": 670, "韩立": 556, "李慕婉": 582}

# 根据key获取value
score = dict1["王林"]
print(score)  # 输出: 670

# 如果key不存在，会报错
# score = dict1["张三"]  # KeyError: '张三'
</code></pre>
<h2>6.2. 字典常用操作</h2>
<table>
<thead>
<tr>
<th>类型</th>
<th>操作</th>
<th>含义</th>
<th>样例</th>
</tr>
</thead>
<tbody>
<tr>
<td>添加</td>
<td><code>字典名称[key] = value</code></td>
<td>往指定字典中添加key-value键值对</td>
<td><code>dict1["涛哥"] = 688</code></td>
</tr>
<tr>
<td>删除</td>
<td><code>字典名称.pop(key)</code></td>
<td>删除字典中指定的key，并返回该key对应的value</td>
<td><code>score = dict1.pop("涛哥")</code></td>
</tr>
<tr>
<td></td>
<td><code>del 字典名称[key]</code></td>
<td>删除字典中指定的键值对</td>
<td><code>del dict1["涛哥"]</code></td>
</tr>
<tr>
<td>修改</td>
<td><code>字典名称[key] = value</code></td>
<td>修改字典中指定的key对应的值</td>
<td><code>dict1["小智"] = 658</code></td>
</tr>
<tr>
<td>查询</td>
<td><code>字典名称[key]</code></td>
<td>根据key获取value</td>
<td><code>dict1["涛哥"]</code></td>
</tr>
<tr>
<td></td>
<td><code>字典名称.get(key)</code></td>
<td>根据key获取value</td>
<td><code>dict1.get("涛哥")</code></td>
</tr>
<tr>
<td></td>
<td><code>字典名称.keys()</code></td>
<td>获取所有的key</td>
<td><code>dict1.keys()</code></td>
</tr>
<tr>
<td></td>
<td><code>字典名称.values()</code></td>
<td>获取所有的value</td>
<td><code>dict1.values()</code></td>
</tr>
<tr>
<td></td>
<td><code>字典名称.items()</code></td>
<td>获取所有的key-value键值对</td>
<td><code>dict1.items()</code></td>
</tr>
</tbody>
</table>
<h3>6.2.1. 添加与修改</h3>
<pre><code>dict1 = {"小智": 675, "李思": 608, "李琪": 478}

# 添加元素
dict1["小黑"] = 545
print(dict1)  # 输出: {'小智': 675, '李思': 608, '李琪': 478, '小黑': 545}

# 修改元素
dict1["李思"] = 650
print(dict1)  # 输出: {'小智': 675, '李思': 650, '李琪': 478, '小黑': 545}
</code></pre>
<h3>6.2.2. 删除元素</h3>
<pre><code>dict1 = {"小智": 675, "李思": 608, "李琪": 478, "小黑": 545}

# 删除指定key
del dict1["李琪"]
print(dict1)  # 输出: {'小智': 675, '李思': 608, '小黑': 545}

# 删除指定key并返回value
score = dict1.pop("小黑")
print(score)  # 输出: 545
print(dict1)  # 输出: {'小智': 675, '李思': 608}
</code></pre>
<h3>6.2.3. 查询元素</h3>
<pre><code>dict1 = {"小智": 675, "李思": 608, "李琪": 478}

# 根据key获取value
print(dict1["小智"])  # 输出: 675

# 使用get方法获取value（key不存在时返回None）
print(dict1.get("李思"))  # 输出: 608
print(dict1.get("张三"))  # 输出: None

# 获取所有key
print(dict1.keys())  # 输出: dict_keys(['小智', '李思', '李琪'])

# 获取所有value
print(dict1.values())  # 输出: dict_values([675, 608, 478])

# 获取所有键值对
print(dict1.items())  # 输出: dict_items([('小智', 675), ('李思', 608), ('李琪', 478)])
</code></pre>
<h2>6.3. 字典遍历</h2>
<p>字典支持for循环遍历：</p>
<pre><code>dict1 = {"小智": 675, "李思": 608, "李琪": 478}

# 遍历key
for key in dict1:
    print(key, dict1[key])

# 遍历key（推荐写法）
for key in dict1.keys():
    print(key, dict1[key])

# 遍历value
for value in dict1.values():
    print(value)

# 遍历键值对
for key, value in dict1.items():
    print(key, value)
</code></pre>
<h2>6.4. 案例实操</h2>
<h3>6.4.1. 案例1：购物车管理系统</h3>
<p>需求：开发一个购物车管理系统，实现商品信息的添加、修改、删除、查询功能。系统使用字典结构存储商品数据，通过控制台菜单与用户交互。</p>
<pre><code># 购物车字典，key是商品名称，value是包含价格和数量的字典
shopping_cart = {}

while True:
    print("=" * 30)
    print("欢迎使用购物车系统")
    print("1. 添加购物车")
    print("2. 修改购物车")
    print("3. 删除购物车")
    print("4. 查询购物车")
    print("5. 退出购物车")
    print("=" * 30)
    
    choice = input("请选择要执行的操作(1-5): ")
    
    if choice == "1":
        # 添加购物车
        name = input("请输入商品名称: ")
        price = float(input("请输入商品价格: "))
        num = int(input("请输入商品数量: "))
        shopping_cart[name] = {"price": price, "num": num}
        print("商品添加成功！")
    
    elif choice == "2":
        # 修改购物车
        name = input("请输入要修改的商品名称: ")
        if name in shopping_cart:
            price = float(input("请输入新的商品价格: "))
            num = int(input("请输入新的商品数量: "))
            shopping_cart[name] = {"price": price, "num": num}
            print("商品修改成功！")
        else:
            print("商品不存在！")
    
    elif choice == "3":
        # 删除购物车
        name = input("请输入要删除的商品名称: ")
        if name in shopping_cart:
            del shopping_cart[name]
            print("商品删除成功！")
        else:
            print("商品不存在！")
    
    elif choice == "4":
        # 查询购物车
        if not shopping_cart:
            print("购物车为空！")
        else:
            print("购物车商品列表:")
            total_price = 0
            for name, info in shopping_cart.items():
                price = info["price"]
                num = info["num"]
                item_total = price * num
                total_price += item_total
                print(f"商品名称: {name}, 商品价格: {price}, 商品数量: {num}, 小计: {item_total}")
            print(f"购物车总价: {total_price}")
    
    elif choice == "5":
        # 退出购物车
        print("感谢使用购物车系统，再见！")
        break
    
    else:
        print("输入错误，请重新选择！")
</code></pre>
<h1>7. 本章总结</h1>
<h2>7.1. 数据容器总结</h2>
<ol>
<li><strong>列表（list）</strong>：有序、可重复、可修改，支持索引和切片，适用于存储有序可重复的数据集合。</li>
<li><strong>字符串（str）</strong>：有序、可重复、不可修改，支持索引和切片，适用于文本处理。</li>
<li><strong>元组（tuple）</strong>：有序、可重复、不可修改，支持索引和切片，适用于存储固定数据记录。</li>
<li><strong>集合（set）</strong>：无序、不可重复、可修改，不支持索引和切片，适用于去重数据集合。</li>
<li><strong>字典（dict）</strong>：有序（3.7+）、key不可重复、可修改，不支持索引和切片，适用于存储键值对数据。</li>
</ol>
<h2>7.2. 常用操作总结</h2>
<ol>
<li><strong>索引访问</strong>：列表、字符串、元组支持通过索引访问元素，正向索引从0开始，反向索引从-1开始。</li>
<li><strong>切片操作</strong>：列表、字符串、元组支持切片操作，语法为<code>[开始索引:结束索引:步长]</code>。</li>
<li><strong>添加元素</strong>：列表使用<code>append()</code>和<code>insert()</code>，集合使用<code>add()</code>，字典使用<code>字典[key] = value</code>。</li>
<li><strong>删除元素</strong>：列表使用<code>remove()</code>和<code>pop()</code>，集合使用<code>remove()</code>和<code>pop()</code>，字典使用<code>del</code>和<code>pop()</code>。</li>
<li><strong>遍历</strong>：所有数据容器都支持for循环遍历，字典可以通过<code>keys()</code>、<code>values()</code>和<code>items()</code>遍历。</li>
<li><strong>推导式</strong>：列表、集合、字典都支持推导式，可以快速生成数据容器。</li>
</ol>
<h2>7.3. 选择建议</h2>
<ul>
<li>如果需要存储有序的、可修改的数据，使用<strong>列表</strong>。</li>
<li>如果需要处理文本数据，使用<strong>字符串</strong>。</li>
<li>如果需要存储固定不变的数据，使用<strong>元组</strong>。</li>
<li>如果需要对数据进行去重处理，使用<strong>集合</strong>。</li>
<li>如果需要通过键快速查找值，使用<strong>字典</strong>。</li>
</ul>
]]></content>
    <author><name>Hygen</name></author>
  </entry>
  <entry>
    <title>Python核心语法04-函数</title>
    <link href="https://www.hygen.red/posts/pylearning/001_python%E6%A0%B8%E5%BF%83%E8%AF%AD%E6%B3%95/004%E5%87%BD%E6%95%B0/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/pylearning/001_python%E6%A0%B8%E5%BF%83%E8%AF%AD%E6%B3%95/004%E5%87%BD%E6%95%B0/</id>
    <published>2026-05-24T00:00:00.000Z</published>
    <updated>2026-05-24T00:00:00.000Z</updated>
    <summary>Python函数学习</summary>
    <content type="html"><![CDATA[<h1>1. 函数基础</h1>
<h2>1.1. 函数定义</h2>
<h3>1.1.1. 什么是函数</h3>
<p>函数是组织好的、可重复使用的、用来实现特定功能的代码片段。</p>
<p>在前面的学习中，我们已经使用过一些 Python 内置函数，例如：</p>
<table>
<thead>
<tr>
<th>函数</th>
<th>作用</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>input()</code></td>
<td>获取键盘输入</td>
</tr>
<tr>
<td><code>print()</code></td>
<td>输出内容到控制台</td>
</tr>
<tr>
<td><code>max()</code></td>
<td>获取最大值</td>
</tr>
<tr>
<td><code>min()</code></td>
<td>获取最小值</td>
</tr>
<tr>
<td><code>len()</code></td>
<td>获取长度</td>
</tr>
<tr>
<td><code>sum()</code></td>
<td>求和</td>
</tr>
</tbody>
</table>
<p>这些函数都是 Python 提前定义好的，可以直接调用、重复使用，并完成特定功能。</p>
<h3>1.1.2. 函数定义与调用</h3>
<p>定义函数使用 <code>def</code> 关键字，基本格式如下：</p>
<pre><code># 定义函数
def 函数名(参数列表):
    函数体
    ......
    return 返回值

# 调用函数
函数名(参数)
</code></pre>
<p>其中，参数列表和返回值不是必须的，需要根据实际需求决定。</p>
<h3>1.1.3. 简单示例</h3>
<pre><code># 定义函数
def out_line():
    print("---------------------------")

# 调用函数
out_line()
</code></pre>
<p>输出结果：</p>
<pre><code>---------------------------
</code></pre>
<h3>1.1.4. 函数使用注意事项</h3>
<ol>
<li>函数必须先定义，再调用。</li>
<li>函数定义时，函数体代码不会立即执行。</li>
<li>只有调用函数时，函数体中的代码才会执行。</li>
<li>Python 通过缩进表示函数体的归属关系。</li>
</ol>
<h2>1.2. 函数的参数与返回值</h2>
<h3>1.2.1. 参数与返回值的作用</h3>
<p>在定义函数时，可以根据业务需求设置参数和返回值。</p>
<ul>
<li>参数：函数运行时需要接收的数据。</li>
<li>返回值：函数运行后返回给调用者的结果。</li>
</ul>
<p>基本格式如下：</p>
<pre><code>def 函数名(参数列表):
    函数体
    return 返回值
</code></pre>
<h3>1.2.2. 单个参数与返回值</h3>
<p>示例：计算圆的面积。</p>
<pre><code>def circle_area(r):
    area = 3.14 * r * r
    return area

c_area = circle_area(10)
print(c_area)
</code></pre>
<p>输出结果：</p>
<pre><code>314.0
</code></pre>
<p>说明：</p>
<ul>
<li><code>r</code> 是函数的参数，表示圆的半径。</li>
<li><code>return area</code> 表示将计算结果返回。</li>
<li><code>print(c_area)</code> 才会把结果输出到控制台。</li>
</ul>
<h3>1.2.3. 多个参数与返回值</h3>
<p>示例：计算长方形面积。</p>
<pre><code>def rectangle_area(l, w):
    area = l * w
    return area

r_area = rectangle_area(20, 10)
print(r_area)
</code></pre>
<p>输出结果：</p>
<pre><code>200
</code></pre>
<p>注意：多个参数之间使用英文逗号 <code>,</code> 分隔。</p>
<h3>1.2.4. 形参与实参</h3>
<table>
<thead>
<tr>
<th>名称</th>
<th>含义</th>
</tr>
</thead>
<tbody>
<tr>
<td>形参</td>
<td>函数定义时括号中的参数，只能在函数内部使用</td>
</tr>
<tr>
<td>实参</td>
<td>函数调用时实际传入的数据</td>
</tr>
</tbody>
</table>
<p>示例：</p>
<pre><code>def circle_area(r):
    return 3.14 * r * r

print(circle_area(10))
</code></pre>
<p>其中：</p>
<ul>
<li><code>r</code> 是形参。</li>
<li><code>10</code> 是实参。</li>
</ul>
<h3>1.2.5. 多个返回值</h3>
<p>Python 函数可以返回多个值，多个返回值会被封装成元组。</p>
<pre><code>def calc(a, b):
    return a + b, a - b, a * b

result = calc(10, 5)
print(result)
</code></pre>
<p>输出结果：</p>
<pre><code>(15, 5, 50)
</code></pre>
<p>也可以通过元组解包接收多个返回值：</p>
<pre><code>x, y, z = calc(10, 5)
print(x)
print(y)
print(z)
</code></pre>
<p>输出结果：</p>
<pre><code>15
5
50
</code></pre>
<h3>1.2.6. return 与 print 的区别</h3>
<p><code>return</code> 只是把结果返回给函数调用处，并不会直接输出内容。</p>
<p>如果想在控制台看到结果，需要结合 <code>print()</code> 使用。</p>
<pre><code>def add(a, b):
    return a + b

result = add(10, 20)
print(result)
</code></pre>
<p>输出结果：</p>
<pre><code>30
</code></pre>
<h2>1.3. 函数说明文档</h2>
<h3>1.3.1. 什么是函数说明文档</h3>
<p>函数说明文档，也叫 Docstring，是写在函数开头、用三引号包裹的字符串。</p>
<p>它通常用于说明：</p>
<ol>
<li>函数的功能。</li>
<li>参数的含义。</li>
<li>返回值的含义。</li>
</ol>
<p>良好的函数说明文档可以让代码更容易理解、使用和维护。</p>
<h3>1.3.2. 函数说明文档示例</h3>
<pre><code>def circle_area_len(r):
    """
    该函数用于根据圆的半径，计算圆的面积和圆的周长
    :param r: 圆的半径
    :return: 圆的面积，圆的周长
    """
    return 3.14 * r * r, 2 * 3.14 * r

al = circle_area_len(10)
print(al)
</code></pre>
<p>输出结果：</p>
<pre><code>(314.0, 62.800000000000004)
</code></pre>
<h3>1.3.3. 查看函数说明文档</h3>
<p>可以使用 <code>help()</code> 函数查看函数说明文档：</p>
<pre><code>help(circle_area_len)
</code></pre>
<p>在开发工具中，也可以将鼠标悬浮在函数上查看文档说明。</p>
<h3>1.3.4. 函数嵌套调用</h3>
<p>函数嵌套调用指的是：在一个函数中调用另一个函数。</p>
<p>函数调用遵循栈结构，特点是：</p>
<blockquote>
<p>最后被调用的函数最先返回。</p>
</blockquote>
<p>也就是 LIFO：</p>
<blockquote>
<p>Last In First Out，后进先出。</p>
</blockquote>
<p>示例代码：</p>
<pre><code>def function_a():
    print("a ... before")
    function_b()
    print("a ... after")

def function_b():
    print("b ... before")
    function_c()
    print("b ... after")

def function_c():
    print("c ...")

function_a()
</code></pre>
<p>输出结果：</p>
<pre><code>a ... before
b ... before
c ...
b ... after
a ... after
</code></pre>
<p>调用过程如下：</p>
<pre><code>function_a() 开始
  │
  ├─ 打印 "a ... before"
  │
  ├─ 调用 function_b()
  │   │
  │   ├─ 打印 "b ... before"
  │   │
  │   ├─ 调用 function_c()
  │   │   │
  │   │   └─ 打印 "c ..."
  │   │
  │   ├─ 打印 "b ... after"
  │   │
  │   └─ function_b() 返回
  │
  ├─ 打印 "a ... after"
  │
  └─ function_a() 返回
</code></pre>
<h2>1.4. 案例实操</h2>
<h3>1.4.1. 案例1：计算三角形面积</h3>
<p>需求：定义一个函数，根据传入的底和高计算三角形面积。</p>
<p>三角形面积公式：</p>
<pre><code>面积 = 底 * 高 / 2
</code></pre>
<p>代码实现：</p>
<pre><code>def triangle_area(b, h):
    """
    该函数用于根据底和高计算三角形的面积
    :param b: 三角形的底
    :param h: 三角形的高
    :return: 三角形的面积
    """
    return (b * h) / 2

print(triangle_area(10, 20))
</code></pre>
<p>输出结果：</p>
<pre><code>100.0
</code></pre>
<h3>1.4.2. 案例2：统计元音字母个数</h3>
<p>需求：定义一个函数，计算传入字符串中元音字母的个数。</p>
<p>元音字母包括：</p>
<pre><code>aeiouAEIOU
</code></pre>
<p>代码实现：</p>
<pre><code>def count_vowel(s):
    """
    该函数用于统计指定字符串中元音字母的个数
    :param s: 需要统计的字符串
    :return: 元音字母的个数
    """
    count = 0
    for i in s:
        if i in "aeiouAEIOU":
            count += 1
    return count

print(count_vowel("hello world"))
</code></pre>
<p>输出结果：</p>
<pre><code>3
</code></pre>
<h3>1.4.3. 案例3：统计成绩信息</h3>
<p>需求：定义一个函数，计算传入成绩列表中的最高分、最低分和平均分，并返回。</p>
<pre><code>def calc_score(scores):
    """
    该函数用于计算班级学员成绩列表中的最高分、最低分和平均分
    :param scores: 成绩列表
    :return: 最高分、最低分、平均分
    """
    max_score = max(scores)
    min_score = min(scores)
    avg_score = sum(scores) / len(scores)
    avg_score = round(avg_score, 1)
    return max_score, min_score, avg_score

print(calc_score([672, 435, 544, 567, 705, 668, 634, 527, 540, 525, 645, 621, 589, 612]))
</code></pre>
<p>输出结果：</p>
<pre><code>(705, 435, 591.7)
</code></pre>
<h3>1.4.4. 练习1：根据分数计算等级</h3>
<p>规则如下：</p>
<table>
<thead>
<tr>
<th>分数范围</th>
<th>等级</th>
</tr>
</thead>
<tbody>
<tr>
<td>分数 &gt;= 90</td>
<td>A</td>
</tr>
<tr>
<td>分数 &gt;= 75</td>
<td>B</td>
</tr>
<tr>
<td>分数 &gt;= 60</td>
<td>C</td>
</tr>
<tr>
<td>分数 &lt; 60</td>
<td>D</td>
</tr>
</tbody>
</table>
<p>代码实现：</p>
<pre><code>def calc_grade(score):
    if score &gt;= 90:
        return "A"
    elif score &gt;= 75:
        return "B"
    elif score &gt;= 60:
        return "C"
    else:
        return "D"

print(calc_grade(100))
print(calc_grade(50))
print(calc_grade(70))
</code></pre>
<p>输出结果：</p>
<pre><code>A
D
C
</code></pre>
<h3>1.4.5. 练习2：判断回文串</h3>
<p>回文串指正着读和反着读都一样的字符串。</p>
<p>例如：</p>
<pre><code>level
radar
黄山落叶松叶落山黄
</code></pre>
<p>代码实现：</p>
<pre><code>def is_palindrome(s):
    return s == s[::-1]

print(is_palindrome("level"))
print(is_palindrome("hello"))
</code></pre>
<p>输出结果：</p>
<pre><code>True
False
</code></pre>
<h3>1.4.6. 练习3：秒数转换为时分秒</h3>
<p>需求：定义一个函数，将传入的秒数转换为小时、分钟、秒。</p>
<pre><code>def time_change(total_seconds):
    """
    该函数用于时间转换，将秒转换为小时、分钟、秒
    :param total_seconds: 总秒数
    :return: 小时，分钟，秒
    """
    hours = total_seconds // 3600
    minutes = (total_seconds - hours * 3600) // 60
    seconds = total_seconds - hours * 3600 - minutes * 60
    return hours, minutes, seconds

print(time_change(10000))

h, m, s = time_change(10000)
print(f"10000秒转换为{h}小时{m}分{s}秒")
</code></pre>
<p>输出结果：</p>
<pre><code>(2, 46, 40)
10000秒转换为2小时46分40秒
</code></pre>
<h3>1.4.7. 练习4：判断三角形类型</h3>
<p>需求：根据传入的三角形三条边，判断三角形类型。</p>
<p>类型包括：</p>
<ol>
<li>等边三角形。</li>
<li>等腰三角形。</li>
<li>普通三角形。</li>
<li>不能构成三角形。</li>
</ol>
<p>代码实现：</p>
<pre><code>def triangle_type(a, b, c):
    if a + b &lt;= c or a + c &lt;= b or b + c &lt;= a:
        return "不能构成三角形"
    elif a == b == c:
        return "等边三角形"
    elif a == b or a == c or b == c:
        return "等腰三角形"
    else:
        return "普通三角形"

print(triangle_type(3, 3, 3))
print(triangle_type(3, 3, 5))
print(triangle_type(3, 4, 5))
print(triangle_type(1, 2, 3))
</code></pre>
<h1>2. 函数进阶</h1>
<h2>2.1. 函数变量的作用域</h2>
<h3>2.1.1. 什么是变量作用域</h3>
<p>变量作用域指的是变量的作用范围，也就是变量在哪里可以使用，在哪里不可以使用。</p>
<p>变量可以分为：</p>
<table>
<thead>
<tr>
<th>类型</th>
<th>定义位置</th>
<th>作用范围</th>
</tr>
</thead>
<tbody>
<tr>
<td>全局变量</td>
<td>函数外部</td>
<td>整个文件中都可以使用</td>
</tr>
<tr>
<td>局部变量</td>
<td>函数内部</td>
<td>只能在函数内部使用</td>
</tr>
</tbody>
</table>
<h3>2.1.2. 全局变量与局部变量</h3>
<pre><code>num = 100

def circle_area(r):
    pi = 3.14
    area = pi * r * r
    return area

count = 0

c_area = circle_area(10)
print(c_area)
</code></pre>
<p>说明：</p>
<ul>
<li><code>num</code> 和 <code>count</code> 是全局变量。</li>
<li><code>pi</code> 和 <code>area</code> 是局部变量。</li>
<li>局部变量只能在函数内部使用。</li>
<li>函数执行完毕后，局部变量会被销毁。</li>
</ul>
<h3>2.1.3. global 关键字</h3>
<p><code>global</code> 关键字用于在函数内部声明使用全局变量。</p>
<p>不使用 <code>global</code> 的情况：</p>
<pre><code>num1 = 1

def fun1():
    num1 = 100
    print(num1)

fun1()
print(num1)
</code></pre>
<p>输出结果：</p>
<pre><code>100
1
</code></pre>
<p>说明：函数内部的 <code>num1</code> 是局部变量，不会影响外部的全局变量。</p>
<p>使用 <code>global</code> 的情况：</p>
<pre><code>num1 = 1

def fun1():
    global num1
    num1 = 100
    print(num1)

fun1()
print(num1)
</code></pre>
<p>输出结果：</p>
<pre><code>100
100
</code></pre>
<p>说明：使用 <code>global num1</code> 后，函数内部修改的是全局变量 <code>num1</code>。</p>
<h3>2.1.4. global 使用注意事项</h3>
<ol>
<li>使用 <code>global</code> 时，要先声明，再使用。</li>
<li>尽量避免在函数中大量修改全局变量。</li>
<li>优先通过函数参数和返回值传递数据。</li>
<li><code>global</code> 常用于程序状态、配置和计数器等场景。</li>
</ol>
<h2>2.2. 函数参数详解</h2>
<h3>2.2.1. 传参方式</h3>
<p>传参方式指的是：调用函数时，传递实参的方式。</p>
<p>常见传参方式包括：</p>
<ol>
<li>位置参数。</li>
<li>关键字参数。</li>
<li>默认参数。</li>
<li>不定长参数。</li>
</ol>
<h3>2.2.2. 位置参数</h3>
<p>位置参数指的是：调用函数时，按照函数定义时参数的位置顺序传递数据。</p>
<pre><code>def reg_stu(name, age, gender, city):
    print(f"注册成功,姓名:{name}, 年龄:{age}, 性别:{gender}, 城市:{city}")
    return {"name": name, "age": age, "gender": gender, "city": city}

stu = reg_stu("张三", 18, "男", "北京")
print(stu)
</code></pre>
<p>输出结果：</p>
<pre><code>注册成功,姓名:张三, 年龄:18, 性别:男, 城市:北京
{'name': '张三', 'age': 18, 'gender': '男', 'city': '北京'}
</code></pre>
<p>位置参数的特点：</p>
<ol>
<li>写法简洁。</li>
<li>参数顺序必须完全一致。</li>
<li>参数较多时可读性较差，容易传错。</li>
</ol>
<h3>2.2.3. 关键字参数</h3>
<p>关键字参数指的是：调用函数时，通过 <code>形参名=值</code> 的方式传递参数。</p>
<pre><code>def reg_stu(name, age, gender, city):
    print(f"注册成功,姓名:{name}, 年龄:{age}, 性别:{gender}, 城市:{city}")
    return {"name": name, "age": age, "gender": gender, "city": city}

stu = reg_stu(name="张三", age=18, gender="男", city="北京")
print(stu)

stu2 = reg_stu(gender="男", name="王五", city="上海", age=22)
print(stu2)
</code></pre>
<p>关键字参数的特点：</p>
<ol>
<li>参数顺序没有要求。</li>
<li>可读性更强。</li>
<li>参数较多时更容易维护。</li>
</ol>
<h3>2.2.4. 位置参数与关键字参数混用</h3>
<p>位置参数和关键字参数可以混用，但必须满足：</p>
<blockquote>
<p>位置参数在前，关键字参数在后。</p>
</blockquote>
<p>正确写法：</p>
<pre><code>stu = reg_stu("赵四", 28, gender="男", city="上海")
print(stu)
</code></pre>
<p>错误写法：</p>
<pre><code># 错误：位置参数不能放在关键字参数后面
stu = reg_stu(name="赵四", 28, gender="男", city="上海")
</code></pre>
<h3>2.2.5. 两种传参方式的使用建议</h3>
<table>
<thead>
<tr>
<th>传参方式</th>
<th>优点</th>
<th>缺点</th>
<th>适用场景</th>
</tr>
</thead>
<tbody>
<tr>
<td>位置参数</td>
<td>简洁</td>
<td>可读性较差，容易出错</td>
<td>参数较少，且顺序自然</td>
</tr>
<tr>
<td>关键字参数</td>
<td>可读性强，易维护</td>
<td>代码略繁琐</td>
<td>参数较多，或容易混淆的场景</td>
</tr>
</tbody>
</table>
<p>建议：</p>
<blockquote>
<p>如果半年后回头看今天写的代码，不能一眼看出每个参数的含义，就应该使用关键字参数。</p>
</blockquote>
<h3>2.2.6. 默认参数</h3>
<p>默认参数也叫缺省参数，用于在定义函数时为参数提供默认值。</p>
<p>调用函数时：</p>
<ol>
<li>如果没有传入该参数，就使用默认值。</li>
<li>如果传入了该参数，就使用传入的值。</li>
</ol>
<p>示例：</p>
<pre><code>def reg_stu(name, age, gender, city="北京"):
    print(f"注册成功,姓名:{name}, 年龄:{age}, 性别:{gender}, 城市:{city}")
    return {"name": name, "age": age, "gender": gender, "city": city}

stu = reg_stu("张三", 18, "男")
print(stu)

stu = reg_stu("赵四", 22, "男", "深圳")
print(stu)
</code></pre>
<p>输出结果：</p>
<pre><code>注册成功,姓名:张三, 年龄:18, 性别:男, 城市:北京
{'name': '张三', 'age': 18, 'gender': '男', 'city': '北京'}
注册成功,姓名:赵四, 年龄:22, 性别:男, 城市:深圳
{'name': '赵四', 'age': 22, 'gender': '男', 'city': '深圳'}
</code></pre>
<p>默认参数注意事项：</p>
<ol>
<li>默认参数必须放在没有默认值的参数后面。</li>
<li>一个函数可以设置多个默认参数。</li>
<li>调用函数时，如果传递了默认参数对应的值，就会覆盖默认值。</li>
</ol>
<p>错误写法：</p>
<pre><code># 错误：默认参数不能放在普通参数前面
def reg_stu(city="北京", name, age):
    pass
</code></pre>
<p>正确写法：</p>
<pre><code>def reg_stu(name, age, city="北京"):
    pass
</code></pre>
<h3>2.2.7. 不定长参数</h3>
<p>不定长参数也叫可变参数，用于函数定义和调用时参数个数不确定的场景。</p>
<p>不定长参数分为两类：</p>
<table>
<thead>
<tr>
<th>类型</th>
<th>写法</th>
<th>接收数据类型</th>
<th>作用</th>
</tr>
</thead>
<tbody>
<tr>
<td>不定长位置参数</td>
<td><code>*args</code></td>
<td>元组</td>
<td>接收多个位置参数</td>
</tr>
<tr>
<td>不定长关键字参数</td>
<td><code>**kwargs</code></td>
<td>字典</td>
<td>接收多个关键字参数</td>
</tr>
</tbody>
</table>
<h3>2.2.8. 不定长位置参数：*args</h3>
<p><code>*args</code> 会接收所有匹配的位置参数，并封装成一个元组。</p>
<pre><code>def calc_data(*args):
    min_data = min(args)
    max_data = max(args)
    avg_data = sum(args) / len(args)
    return min_data, max_data, round(avg_data, 1)

data = calc_data(10, 20, 30, 40, 50, 60, 70, 80, 90, 100)
print(data)

data = calc_data(100, 200, 300, 400, 500)
print(data)
</code></pre>
<p>输出结果：</p>
<pre><code>(10, 100, 55.0)
(100, 500, 300.0)
</code></pre>
<p>注意：</p>
<ol>
<li><code>args</code> 是元组类型。</li>
<li><code>args</code> 只是约定俗成的变量名，不是关键字。</li>
<li>可以写成 <code>*data</code>，但一般推荐使用 <code>*args</code>。</li>
</ol>
<h3>2.2.9. 不定长关键字参数：**kwargs</h3>
<p><code>**kwargs</code> 会接收所有关键字参数，并封装成一个字典。</p>
<pre><code>def calc_data(*args, **kwargs):
    min_data = min(args)
    max_data = max(args)
    avg_data = sum(args) / len(args)

    if kwargs.get("round"):
        avg_data = round(avg_data, kwargs.get("round"))

    return min_data, max_data, avg_data

data = calc_data(100, 200, 300, 400, round=2, count=0)
print(data)

data = calc_data(33, 11, 28, 91, 32, 75, 49)
print(data)
</code></pre>
<p>输出结果：</p>
<pre><code>(100, 400, 250.0)
(11, 91, 45.57142857142857)
</code></pre>
<p>注意：</p>
<ol>
<li><code>kwargs</code> 是字典类型。</li>
<li><code>kwargs</code> 只是约定俗成的变量名，不是关键字。</li>
<li>可以写成 <code>**options</code>，但一般推荐使用 <code>**kwargs</code>。</li>
</ol>
<h3>2.2.10. *args 与 **kwargs 的应用场景</h3>
<table>
<thead>
<tr>
<th>参数类型</th>
<th>适用场景</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>*args</code></td>
<td>处理数量不确定的数据</td>
</tr>
<tr>
<td><code>**kwargs</code></td>
<td>处理数量不确定的选项或配置参数</td>
</tr>
</tbody>
</table>
<h3>2.2.11. 函数的参数类型</h3>
<p>函数的参数可以是普通数据，也可以是函数。</p>
<p>普通参数包括：</p>
<ol>
<li>数字。</li>
<li>布尔。</li>
<li>字符串。</li>
<li>列表。</li>
<li>元组。</li>
<li>集合。</li>
<li>字典。</li>
</ol>
<p>示例1：数字作为参数。</p>
<pre><code>def circle_area(r):
    area = 3.14 * r ** 2
    return area

area = circle_area(10)
print(area)
</code></pre>
<p>示例2：列表作为参数。</p>
<pre><code>def calc_score(score_list):
    max_s = max(score_list)
    min_s = min(score_list)
    avg_s = round(sum(score_list) / len(score_list), 1)
    return max_s, min_s, avg_s

s_list = [589, 609, 605, 643, 677, 455, 477, 489, 503]
max_score, min_score, avg_score = calc_score(s_list)

print(max_score, min_score, avg_score)
</code></pre>
<h3>2.2.12. 函数作为参数</h3>
<p>函数本身也可以作为另一个函数的参数。</p>
<p>这种写法常用于把“要执行的逻辑”传递给函数。</p>
<pre><code>def add(x, y):
    return x + y

def subtract(x, y):
    return x - y

def calc(x, y, oper):
    return oper(x, y)

result = calc(10, 20, add)
print(result)
</code></pre>
<p>输出结果：</p>
<pre><code>30
</code></pre>
<p>说明：</p>
<ul>
<li><code>x</code> 和 <code>y</code> 传递的是实际要计算的数据。</li>
<li><code>oper</code> 传递的是函数中封装的计算逻辑。</li>
</ul>
<h2>2.3. 匿名函数</h2>
<h3>2.3.1. 什么是匿名函数</h3>
<p>匿名函数指没有名称的函数，需要通过 <code>lambda</code> 表达式声明。</p>
<p>匿名函数适合逻辑简单、只在一个地方使用的场景。</p>
<h3>2.3.2. 匿名函数语法</h3>
<pre><code>lambda 参数列表: 函数体
</code></pre>
<p>注意：</p>
<ol>
<li>匿名函数通常只能写单行表达式。</li>
<li>匿名函数可以有返回值，也可以没有返回值。</li>
<li>返回结果时不需要写 <code>return</code>，表达式的结果就是返回值。</li>
</ol>
<h3>2.3.3. 命名函数写法</h3>
<pre><code>def out_line():
    print("-------------------------")

def add(x, y):
    return x + y

out_line()
print(add(10, 20))
</code></pre>
<h3>2.3.4. 匿名函数写法</h3>
<pre><code>out_line = lambda: print("-------------------------")
add = lambda x, y: x + y

out_line()
print(add(100, 200))
</code></pre>
<p>输出结果：</p>
<pre><code>-------------------------
300
</code></pre>
<h3>2.3.5. 命名函数与匿名函数的选择</h3>
<table>
<thead>
<tr>
<th>函数类型</th>
<th>适用场景</th>
</tr>
</thead>
<tbody>
<tr>
<td>命名函数</td>
<td>逻辑复杂、需要多步操作、需要重复使用、需要写说明文档</td>
</tr>
<tr>
<td>匿名函数</td>
<td>逻辑简单、只在一个地方使用、常作为高阶函数参数</td>
</tr>
</tbody>
</table>
<p>建议：</p>
<blockquote>
<p>代码的可读性和可维护性比简洁性更重要。</p>
</blockquote>
<h2>2.4. 案例实操</h2>
<h3>2.4.1. 案例1：N 的阶乘</h3>
<p>需求：定义一个函数，根据传入的数字，计算该数字的阶乘。</p>
<p>阶乘示例：</p>
<pre><code>8! = 8 * 7 * 6 * 5 * 4 * 3 * 2 * 1
7! = 7 * 6 * 5 * 4 * 3 * 2 * 1
6! = 6 * 5 * 4 * 3 * 2 * 1
</code></pre>
<p>递归公式：</p>
<pre><code>f(n) = n * f(n - 1)
f(1) = 1
</code></pre>
<p>代码实现：</p>
<pre><code>def factorial(n):
    if n == 1:
        return 1
    return n * factorial(n - 1)

print(factorial(8))
</code></pre>
<p>输出结果：</p>
<pre><code>40320
</code></pre>
<h3>2.4.2. 案例2：班级成绩统计</h3>
<p>需求：根据输入的班级名称，以及班级中各个学员的考试总分，统计班级平均分、高于平均分的人数和低于平均分的人数。</p>
<pre><code>def calc_grade(class_name, *args):
    """
    根据输入的班级名称，以及班级中各个学员的考试总分，
    统计班级平均分、高于平均分的人数和低于平均分的人数
    :param class_name: 班级名称
    :param args: 班级学员分数，元组类型
    :return: 平均分，高于平均分的人数，低于平均分的人数
    """
    total_score = sum(args)
    avg_score = total_score / len(args)
    above_avg_count = sum(1 for score in args if score &gt; avg_score)
    below_avg_count = sum(1 for score in args if score &lt; avg_score)
    return avg_score, above_avg_count, below_avg_count

avg_score, above_avg_count, below_avg_count = calc_grade(
    "六年级一班",
    650, 683, 706, 593, 623, 634, 712, 588, 562, 470, 611
)

print(f"班级【六年级一班】的平均分是 {avg_score:.2f}, 高于平均分的人数有 {above_avg_count} 人, 低于平均分的人数有 {below_avg_count} 人")
</code></pre>
<h3>2.4.3. 案例3：电商订单计算器</h3>
<p>需求：定义一个函数，根据传入的一批商品信息、优惠信息和运费信息，计算订单总金额。</p>
<p>商品信息格式：</p>
<pre><code>("商品名", 价格, 数量)
</code></pre>
<p>规则：</p>
<ol>
<li>优惠券需要商品金额满 5000 才可以使用。</li>
<li>优惠券金额不能超过商品总价。</li>
<li>积分抵扣需要商品金额满 5000 才可以使用。</li>
<li>100 积分抵扣 1 元。</li>
<li>积分只能整百抵扣。</li>
<li>抵扣金额不能超过商品总价。</li>
</ol>
<p>基础代码示例：</p>
<pre><code>def calc_cart_total_price(*goods, coupon_price=0, point_deduction=0, express_price=0):
    """
    根据商品信息、优惠券、积分抵扣和运费计算购物车总金额
    :param goods: 商品信息
    :param coupon_price: 优惠券金额
    :param point_deduction: 积分抵扣金额
    :param express_price: 运费金额
    :return: 购物车总金额
    """
    total_price = sum(good[1] * good[2] for good in goods)

    if total_price &gt; 5000:
        total_price -= coupon_price

    if total_price &gt; 8000:
        total_price -= point_deduction

    return total_price + express_price

print(calc_cart_total_price(
    ("鼠标", 100, 2),
    ("键盘", 200, 3),
    coupon_price=500,
    point_deduction=1000,
    express_price=50
))
</code></pre>
<h3>2.4.4. 列表推导式与生成器表达式</h3>
<p>列表推导式的写法：</p>
<pre><code>[要插入的值 for i in 数据集 if 条件]
</code></pre>
<p>示例：</p>
<pre><code>nums = [i for i in range(10) if i % 2 == 0]
print(nums)
</code></pre>
<p>输出结果：</p>
<pre><code>[0, 2, 4, 6, 8]
</code></pre>
<p>特点：</p>
<ol>
<li>立即求值。</li>
<li>一次性生成整个列表。</li>
<li>会占用较多内存空间。</li>
</ol>
<p>适用场景：</p>
<ol>
<li>数据量不大。</li>
<li>需要立即生成全部数据。</li>
<li>需要重复使用、多次访问。</li>
</ol>
<p>生成器表达式的写法：</p>
<pre><code>(要插入的值 for i in 数据集 if 条件)
</code></pre>
<p>示例：</p>
<pre><code>nums = (i for i in range(10) if i % 2 == 0)
print(nums)

for num in nums:
    print(num)
</code></pre>
<p>特点：</p>
<ol>
<li>惰性求值。</li>
<li>按需逐个生成元素。</li>
<li>不会一次性把所有元素存入内存。</li>
<li>更节省内存。</li>
</ol>
<p>适用场景：</p>
<ol>
<li>处理大数据集。</li>
<li>避免一次性加载所有数据到内存。</li>
<li>作为 <code>sum()</code>、<code>max()</code>、<code>min()</code> 等函数的参数。</li>
</ol>
<p>示例：</p>
<pre><code>total = sum(i for i in range(1000000))
print(total)
</code></pre>
<h1>3. 类型注解</h1>
<h2>3.1. 基本介绍</h2>
<h3>3.1.1. 什么是类型注解</h3>
<p>类型注解是 Python 中的一种语法特性，用于明确标识变量、函数参数和返回值的数据类型。</p>
<p>使用类型注解的好处：</p>
<ol>
<li>代码结构更清晰。</li>
<li>代码逻辑更安全。</li>
<li>更易维护。</li>
<li>代码自动提示更准确。</li>
<li>可以提前发现潜在问题。</li>
</ol>
<p>注意：</p>
<blockquote>
<p>Python 是动态类型语言，类型注解只是提示，不是强制约束。</p>
</blockquote>
<h3>3.1.2. 变量类型注解</h3>
<p>不使用类型注解：</p>
<pre><code>a = 695
score = 98.5
hobby = "Python"
flag = True
pic = None

names = ["A", "C", "E"]
phones = {"13309091111", "15209109121"}
options = {"count": 0, "total": 0}
goods = ("手机", 5999, 1)
</code></pre>
<p>使用类型注解：</p>
<pre><code>a: int = 695
score: float = 98.5
hobby: str = "Python"
flag: bool = True
pic: None = None

names: list[str] = ["A", "C", "E"]
phones: set[str] = {"13309091111", "15209109121"}
options: dict[str, int] = {"count": 0, "total": 0}
goods: tuple[str, int, int] = ("手机", 5999, 1)
</code></pre>
<h3>3.1.3. 常见类型注解写法</h3>
<table>
<thead>
<tr>
<th>数据类型</th>
<th>写法</th>
</tr>
</thead>
<tbody>
<tr>
<td>整数</td>
<td><code>int</code></td>
</tr>
<tr>
<td>浮点数</td>
<td><code>float</code></td>
</tr>
<tr>
<td>布尔</td>
<td><code>bool</code></td>
</tr>
<tr>
<td>字符串</td>
<td><code>str</code></td>
</tr>
<tr>
<td>空值</td>
<td><code>None</code></td>
</tr>
<tr>
<td>列表</td>
<td><code>list[int]</code>、<code>list[str]</code></td>
</tr>
<tr>
<td>集合</td>
<td><code>set[str]</code></td>
</tr>
<tr>
<td>元组</td>
<td><code>tuple[str, int, int]</code></td>
</tr>
<tr>
<td>字典</td>
<td><code>dict[str, int]</code></td>
</tr>
<tr>
<td>多种类型</td>
<td>`str</td>
</tr>
</tbody>
</table>
<h3>3.1.4. 类型推断</h3>
<p>类型推断指的是 Python 解释器自动推断变量、表达式或函数返回值数据类型的能力。</p>
<pre><code>a = 695
score = 98.5
hobby = "Python"
flag = True
pic = None
</code></pre>
<p>在变量直接赋值、变量运算、容器推导等场景中，解释器可以自动推断类型。</p>
<h3>3.1.5. 类型注解小结</h3>
<p>类型注解的写法：</p>
<pre><code>变量名: 数据类型 = 值
</code></pre>
<p>例如：</p>
<pre><code>name: str = "Tom"
age: int = 18
score: float = 98.5
</code></pre>
<p>类型注解的核心作用不是限制代码运行，而是提升代码的可读性、可维护性和编辑器提示效果。</p>
<h2>3.2. 函数类型注解</h2>
<h3>3.2.1. 函数参数和返回值注解</h3>
<p>为函数添加类型注解，主要是给函数参数和返回值添加类型说明。</p>
<p>基本语法：</p>
<pre><code>def 函数名(参数名: 参数类型) -&gt; 返回值类型:
    函数体
</code></pre>
<h3>3.2.2. 示例1：计算平均分</h3>
<pre><code>def calc(scores: list[int]) -&gt; float:
    return sum(scores) / len(scores)
</code></pre>
<p>说明：</p>
<ul>
<li><code>scores: list[int]</code> 表示参数 <code>scores</code> 是一个整数列表。</li>
<li><code>-&gt; float</code> 表示函数返回值是浮点数。</li>
</ul>
<h3>3.2.3. 示例2：返回多个结果</h3>
<pre><code>def calc_data(scores: list[int]) -&gt; tuple[int, int, float]:
    max_v = max(scores)
    min_v = min(scores)
    avg_v = sum(scores) / len(scores)
    return max_v, min_v, avg_v
</code></pre>
<p>说明：</p>
<ul>
<li>返回值类型是 <code>tuple[int, int, float]</code>。</li>
<li>表示函数返回一个元组，元组中依次是整数、整数、浮点数。</li>
</ul>
<h3>3.2.4. 带类型注解的电商订单计算器</h3>
<pre><code>def calc_cart_total_price(
    *goods: tuple[str, float, int],
    coupon_price: int = 0,
    point_deduction: int = 0,
    express_price: float = 0
) -&gt; float:
    """
    定义一个用于根据商品信息、优惠券、积分抵扣、运费信息计算购物车总金额的函数
    :param goods: 商品信息
    :param coupon_price: 优惠券金额
    :param point_deduction: 积分抵扣积分数
    :param express_price: 运费金额
    :return: 购物车总金额
    """
    total_price = sum(good[1] * good[2] for good in goods)

    if total_price &gt; 5000 and coupon_price &lt;= total_price:
        total_price -= coupon_price

    if total_price &gt; 5000 and point_deduction // 100 &lt;= total_price:
        total_price -= point_deduction // 100

    return total_price + express_price

print(calc_cart_total_price(
    ("鼠标", 88.5, 2),
    ("键盘", 168.9, 3),
    coupon_price=500,
    point_deduction=1000,
    express_price=8.5
))
</code></pre>
<h3>3.2.5. 函数类型注解小结</h3>
<p>函数中类型注解的语法：</p>
<pre><code>def calc_data(scores: list[int]) -&gt; tuple[int, int, float]:
    max_v = max(scores)
    min_v = min(scores)
    avg_v = sum(scores) / len(scores)
    return max_v, min_v, avg_v
</code></pre>
<p>其中：</p>
<ul>
<li><code>scores: list[int]</code> 表示参数类型。</li>
<li><code>-&gt; tuple[int, int, float]</code> 表示返回值类型。</li>
</ul>
<p>对于需要团队协作开发和长期维护的项目，推荐使用类型注解。</p>
<h1>4. 本章总结</h1>
<h2>4.1. 函数基础总结</h2>
<ol>
<li>函数是可重复使用的代码片段。</li>
<li>使用 <code>def</code> 定义函数。</li>
<li>函数必须先定义，再调用。</li>
<li>函数可以有参数，也可以没有参数。</li>
<li>函数可以有返回值，也可以没有返回值。</li>
<li><code>return</code> 只负责返回结果，不负责输出。</li>
<li>输出结果需要使用 <code>print()</code>。</li>
<li>函数说明文档可以提升代码可读性和可维护性。</li>
<li>函数嵌套调用遵循后进先出的调用顺序。</li>
</ol>
<h2>4.2. 函数进阶总结</h2>
<ol>
<li>函数内部定义的变量是局部变量。</li>
<li>函数外部定义的变量是全局变量。</li>
<li><code>global</code> 可以在函数内部声明使用全局变量。</li>
<li>位置参数按顺序传递。</li>
<li>关键字参数按 <code>形参名=值</code> 传递。</li>
<li>默认参数可以给参数设置默认值。</li>
<li><code>*args</code> 用于接收多个位置参数。</li>
<li><code>**kwargs</code> 用于接收多个关键字参数。</li>
<li>函数也可以作为参数传递。</li>
<li>匿名函数使用 <code>lambda</code> 定义。</li>
</ol>
<h2>4.3. 类型注解总结</h2>
<ol>
<li>类型注解可以提高代码可读性和可维护性。</li>
<li>变量可以添加类型注解。</li>
<li>函数参数和返回值也可以添加类型注解。</li>
<li>类型注解只是提示，不是强制约束。</li>
<li>团队协作开发和长期维护项目中，推荐使用类型注解。</li>
</ol>
]]></content>
    <author><name>Hygen</name></author>
  </entry>
  <entry>
    <title>Python核心语法05-模块</title>
    <link href="https://www.hygen.red/posts/pylearning/001_python%E6%A0%B8%E5%BF%83%E8%AF%AD%E6%B3%95/005%E6%A8%A1%E5%9D%97/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/pylearning/001_python%E6%A0%B8%E5%BF%83%E8%AF%AD%E6%B3%95/005%E6%A8%A1%E5%9D%97/</id>
    <published>2026-05-24T00:00:00.000Z</published>
    <updated>2026-05-24T00:00:00.000Z</updated>
    <summary>Python模块学习</summary>
    <content type="html"><![CDATA[<h1>1. Python模块</h1>
<h2>1.1. 导入模块</h2>
<h3>1.1.1. 什么是模块</h3>
<p>Python 模块（module）指的是一个 <code>.py</code> 文件。</p>
<p>一个模块中可以包含：</p>
<ol>
<li>变量。</li>
<li>函数。</li>
<li>类。</li>
<li>可执行代码。</li>
</ol>
<p>简单来说：</p>
<blockquote>
<p>一个 Python 文件就是一个模块，模块是 Python 程序的基本组织单位。</p>
</blockquote>
<p>例如，定义一个 <code>circle_fun.py</code> 文件：</p>
<pre><code># 根据半径计算圆的面积
def circle_area(r):
    pi = 3.14
    area = pi * r * r
    return area

# 根据半径计算圆的周长
def circle_perimeter(r):
    pi = 3.14
    perimeter = 2 * pi * r
    return perimeter
</code></pre>
<p>这个 <code>circle_fun.py</code> 文件就可以看作一个模块。</p>
<h3>1.1.2. 为什么要使用模块</h3>
<p>当代码量比较少时，可以把所有代码写在一个文件中。</p>
<p>但是随着项目变复杂，如果所有代码都写在一个文件中，会出现以下问题：</p>
<ol>
<li>代码太长，不方便阅读。</li>
<li>功能混在一起，不方便维护。</li>
<li>相同功能无法复用。</li>
<li>多人协作时容易互相影响。</li>
</ol>
<p>使用模块可以把不同功能拆分到不同文件中，从而让代码结构更加清晰。</p>
<p>模块的主要作用如下：</p>
<table>
<thead>
<tr>
<th>作用</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>提高代码复用性</td>
<td>一个模块中的函数可以被多个文件重复使用</td>
</tr>
<tr>
<td>降低开发门槛</td>
<td>常用功能可以直接导入别人写好的模块</td>
</tr>
<tr>
<td>避免命名冲突</td>
<td>不同模块中可以有相同名称的函数或变量</td>
</tr>
<tr>
<td>便于维护管理</td>
<td>不同功能拆分到不同文件中，结构更清晰</td>
</tr>
</tbody>
</table>
<h3>1.1.3. 常见内置模块</h3>
<p>Python 提供了很多常用的内置模块，例如：</p>
<table>
<thead>
<tr>
<th>模块</th>
<th>作用</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>math</code></td>
<td>数学计算</td>
</tr>
<tr>
<td><code>os</code></td>
<td>操作系统相关操作</td>
</tr>
<tr>
<td><code>datetime</code></td>
<td>日期时间处理</td>
</tr>
<tr>
<td><code>re</code></td>
<td>正则表达式</td>
</tr>
<tr>
<td><code>random</code></td>
<td>随机数相关操作</td>
</tr>
<tr>
<td><code>sys</code></td>
<td>系统参数相关操作</td>
</tr>
<tr>
<td><code>time</code></td>
<td>时间访问与处理</td>
</tr>
<tr>
<td><code>csv</code></td>
<td>CSV 文件操作</td>
</tr>
</tbody>
</table>
<p>例如，使用 <code>random</code> 模块实现随机点名：</p>
<pre><code>import random

names = [
    "王林", "李慕婉",
    "许立国", "韩立",
    "涛哥", "莫厉海",
    "十三", "虎咆",
    "红蝶", "天运子"
]

print(random.choice(names))
</code></pre>
<p>其中：</p>
<ul>
<li><code>import random</code> 表示导入 <code>random</code> 模块。</li>
<li><code>random.choice(names)</code> 表示从列表中随机选择一个元素。</li>
</ul>
<h3>1.1.4. 模块导入的基本原则</h3>
<p>在使用模块中提供的功能之前，必须先导入模块。</p>
<p>也就是：</p>
<blockquote>
<p>先导入，再使用。</p>
</blockquote>
<p>模块导入语句一般写在 <code>.py</code> 文件的开头。</p>
<h3>1.1.5. import 模块名</h3>
<p>第一种方式是直接导入模块。</p>
<p>语法：</p>
<pre><code>import 模块名
</code></pre>
<p>示例：</p>
<pre><code>import random

num = random.randint(10, 100)
print(num)
</code></pre>
<p>说明：</p>
<ul>
<li><code>import random</code> 表示导入 <code>random</code> 模块。</li>
<li>调用模块中的功能时，需要使用 <code>模块名.功能名</code> 的形式。</li>
<li><code>random.randint(10, 100)</code> 表示生成 10 到 100 之间的随机整数。</li>
</ul>
<p>也可以一次导入多个模块：</p>
<pre><code>import random, os

print(random.randint(1, 10))
print(os.getcwd())
</code></pre>
<p>不过在实际开发中，更推荐分多行导入，代码更清晰：</p>
<pre><code>import random
import os
</code></pre>
<h3>1.1.6. import 模块名 as 别名</h3>
<p>如果模块名比较长，或者想让代码更简洁，可以给模块起别名。</p>
<p>语法：</p>
<pre><code>import 模块名 as 别名
</code></pre>
<p>示例：</p>
<pre><code>import random as rd

num = rd.randint(10, 100)
print(num)
</code></pre>
<p>说明：</p>
<ul>
<li><code>rd</code> 是 <code>random</code> 模块的别名。</li>
<li>使用别名后，调用功能时要写 <code>rd.randint()</code>，不能再写 <code>random.randint()</code>。</li>
</ul>
<h3>1.1.7. from 模块名 import 功能名</h3>
<p>如果只想使用模块中的某几个功能，可以只导入指定功能。</p>
<p>语法：</p>
<pre><code>from 模块名 import 功能名
</code></pre>
<p>示例：</p>
<pre><code>from random import randint, choice

num = randint(10, 100)
print(num)

names = ["张三", "李四", "王五"]
print(choice(names))
</code></pre>
<p>说明：</p>
<ul>
<li><code>from random import randint, choice</code> 表示从 <code>random</code> 模块中导入 <code>randint</code> 和 <code>choice</code>。</li>
<li>导入后可以直接使用功能名，不需要再写 <code>random.</code>。</li>
</ul>
<h3>1.1.8. from 模块名 import 功能名 as 别名</h3>
<p>也可以给导入的某个功能起别名。</p>
<p>语法：</p>
<pre><code>from 模块名 import 功能名 as 别名
</code></pre>
<p>示例：</p>
<pre><code>from random import randint as rint

num = rint(10, 100)
print(num)
</code></pre>
<p>说明：</p>
<ul>
<li><code>rint</code> 是 <code>randint</code> 的别名。</li>
<li>使用别名后，直接调用 <code>rint()</code> 即可。</li>
</ul>
<h3>1.1.9. from 模块名 import *</h3>
<p><code>*</code> 表示导入模块中的所有功能。</p>
<p>语法：</p>
<pre><code>from 模块名 import *
</code></pre>
<p>示例：</p>
<pre><code>from random import *

num = randint(10, 100)
print(num)

names = ["张三", "李四", "王五"]
print(choice(names))
</code></pre>
<p>这种写法虽然简洁，但是不推荐在大型项目中大量使用。</p>
<p>原因是：</p>
<ol>
<li>不容易看出某个功能来自哪个模块。</li>
<li>容易造成命名冲突。</li>
<li>不利于代码维护。</li>
</ol>
<h3>1.1.10. 模块导入方式总结</h3>
<table>
<thead>
<tr>
<th>导入形式</th>
<th>代码样例</th>
<th>调用方式</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>import 模块名</code></td>
<td><code>import random</code></td>
<td><code>random.randint(10, 100)</code></td>
</tr>
<tr>
<td><code>import 模块名 as 别名</code></td>
<td><code>import random as rd</code></td>
<td><code>rd.randint(10, 100)</code></td>
</tr>
<tr>
<td><code>from 模块名 import 功能名</code></td>
<td><code>from random import randint</code></td>
<td><code>randint(10, 100)</code></td>
</tr>
<tr>
<td><code>from 模块名 import 功能名 as 别名</code></td>
<td><code>from random import randint as rint</code></td>
<td><code>rint(10, 100)</code></td>
</tr>
<tr>
<td><code>from 模块名 import *</code></td>
<td><code>from random import *</code></td>
<td><code>randint(10, 100)</code></td>
</tr>
</tbody>
</table>
<p>常用语法可以概括为：</p>
<pre><code>import 模块名 [as 别名]

from 模块名 import 功能名 [as 别名]

from 模块名 import *
</code></pre>
<h2>1.2. 自定义模块</h2>
<h3>1.2.1. 什么是自定义模块</h3>
<p>自定义模块就是程序员自己创建的 <code>.py</code> 文件。</p>
<p>在实际开发中，当项目比较复杂时，为了让代码结构更清晰、更便于维护和复用，通常会把一个项目拆分为多个模块。</p>
<p>例如，一个完整的项目中可能包含：</p>
<pre><code>config.py
session.py
ai_partner.py
</code></pre>
<p>其中：</p>
<table>
<thead>
<tr>
<th>文件</th>
<th>作用</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>config.py</code></td>
<td>保存项目配置</td>
</tr>
<tr>
<td><code>session.py</code></td>
<td>处理会话相关功能</td>
</tr>
<tr>
<td><code>ai_partner.py</code></td>
<td>项目主程序入口</td>
</tr>
</tbody>
</table>
<p>注意：</p>
<blockquote>
<p>每一个 Python 文件都可以作为一个模块，模块名就是文件名。</p>
</blockquote>
<p>例如：</p>
<pre><code>circle_fun.py
</code></pre>
<p>这个模块的模块名就是：</p>
<pre><code>circle_fun
</code></pre>
<p>导入时不要写 <code>.py</code> 后缀：</p>
<pre><code>import circle_fun
</code></pre>
<h3>1.2.2. 自定义模块命名规范</h3>
<p>自定义模块的文件名建议遵守 Python 标识符命名规范：</p>
<ol>
<li>使用英文、数字、下划线。</li>
<li>不能以数字开头。</li>
<li>不要使用 Python 关键字。</li>
<li>建议全部小写。</li>
<li>多个单词之间使用下划线连接。</li>
</ol>
<p>推荐写法：</p>
<pre><code>circle_fun.py
data_query.py
web_service.py
my_config.py
</code></pre>
<p>不推荐写法：</p>
<pre><code>CircleFun.py
2test.py
my-module.py
class.py
</code></pre>
<h3>1.2.3. 自定义模块示例</h3>
<p>假设有一个 <code>circle_fun.py</code> 文件，内容如下：</p>
<pre><code># 根据半径计算圆的面积
def circle_area(r):
    pi = 3.14
    area = pi * r * r
    return area

# 根据半径计算圆的周长
def circle_perimeter(r):
    pi = 3.14
    perimeter = 2 * pi * r
    return perimeter
</code></pre>
<p>然后在 <code>main.py</code> 中导入并使用：</p>
<pre><code>import circle_fun

area = circle_fun.circle_area(10)
perimeter = circle_fun.circle_perimeter(10)

print(area)
print(perimeter)
</code></pre>
<p>输出结果：</p>
<pre><code>314.0
62.800000000000004
</code></pre>
<p>也可以只导入指定功能：</p>
<pre><code>from circle_fun import circle_area, circle_perimeter

area = circle_area(10)
perimeter = circle_perimeter(10)

print(area)
print(perimeter)
</code></pre>
<h3>1.2.4. 项目拆分示例</h3>
<p>假设我们要开发一个“AI树洞”项目，如果把所有代码都写在一个文件中，代码会非常混乱。</p>
<p>可以把不同功能拆分到不同模块中。</p>
<h4>config.py</h4>
<p>用于存放基础配置项：</p>
<pre><code>PAGE_TITLE = "AI树洞"
PAGE_ICON = "🤖"
LAYOUT = "wide"
INITIAL_SIDEBAR_STATE = "expanded"
HELP_URL = "https://www.extremelycoolapp.com/help"
SESSION_LOCATION = "sessions/"
</code></pre>
<h4>session.py</h4>
<p>用于处理会话相关功能：</p>
<pre><code>import os

def load_sessions():
    sessions = []
    if os.path.exists("sessions"):
        for filename in os.listdir("sessions"):
            if filename.endswith(".json"):
                sessions.append(filename[:-5])
    return sorted(sessions, reverse=True)

def save_session(session_name):
    if session_name:
        # 这里省略保存会话的具体逻辑
        pass
</code></pre>
<h4>ai_partner.py</h4>
<p>作为主程序文件，导入并使用其他模块中的功能：</p>
<pre><code>from session import load_sessions, save_session

# 新建会话按钮
if st.button("新建会话", icon="✏️", use_container_width=True):
    if st.session_state.current_session:
        save_session(st.session_state.current_session)

    new_session_name = generate_new_session_name()
    st.session_state.current_session = new_session_name
    st.session_state.messages = []

    save_session(new_session_name)
    st.session_state.sessions = load_sessions()
    st.rerun()
</code></pre>
<p>通过这种拆分方式，可以让每个文件只负责一类功能，项目结构更加清晰。</p>
<h3>1.2.5. <strong>all</strong> 变量</h3>
<p><code>__all__</code> 是一个模块级别的特殊变量。</p>
<p>它的作用是：</p>
<blockquote>
<p>控制 <code>from 模块名 import *</code> 时会导入哪些功能。</p>
</blockquote>
<p>例如，创建一个 <code>my.py</code> 文件：</p>
<pre><code>__all__ = ["log_separator1", "log_separator3", "PI"]

PI = 3.1415926
NAME = "黑马☆涛哥"

def log_separator1():
    print("- " * 30)

def log_separator2():
    print("+ " * 30)

def log_separator3():
    print("# " * 30)

def log_separator4():
    print("* " * 30)
</code></pre>
<p>然后在 <code>test.py</code> 中使用：</p>
<pre><code>from my import *

log_separator1()
log_separator3()

print(PI)
</code></pre>
<p>此时可以使用：</p>
<pre><code>log_separator1
log_separator3
PI
</code></pre>
<p>但是不能直接使用：</p>
<pre><code>log_separator2
log_separator4
NAME
</code></pre>
<p>因为它们没有写在 <code>__all__</code> 中。</p>
<h3>1.2.6. <strong>all</strong> 的注意事项</h3>
<p><code>__all__</code> 只控制这种导入方式：</p>
<pre><code>from 模块名 import *
</code></pre>
<p>它不会影响直接导入指定功能。</p>
<p>例如，即使 <code>NAME</code> 没有写入 <code>__all__</code>，下面这种写法仍然可以使用：</p>
<pre><code>from my import NAME

print(NAME)
</code></pre>
<p>所以，<code>__all__</code> 的作用并不是彻底隐藏模块中的功能，而是控制 <code>import *</code> 时通配导入的范围。</p>
<h3>1.2.7. <strong>name</strong> 变量</h3>
<p><code>__name__</code> 是 Python 中非常重要的内置变量，表示当前模块的名称。</p>
<p>它有两种常见情况：</p>
<table>
<thead>
<tr>
<th>运行方式</th>
<th><code>__name__</code> 的值</th>
</tr>
</thead>
<tbody>
<tr>
<td>当前模块被直接运行</td>
<td><code>"__main__"</code></td>
</tr>
<tr>
<td>当前模块被其他文件导入</td>
<td>模块名，也就是文件名去掉 <code>.py</code> 后缀</td>
</tr>
</tbody>
</table>
<p>例如，创建一个 <code>my.py</code> 文件：</p>
<pre><code>def test():
    print("这是测试代码")

print(__name__)
</code></pre>
<p>如果直接运行 <code>my.py</code>，输出结果是：</p>
<pre><code>__main__
</code></pre>
<p>如果在其他文件中导入 <code>my.py</code>：</p>
<pre><code>import my
</code></pre>
<p>此时 <code>my.py</code> 中的 <code>__name__</code> 输出结果是：</p>
<pre><code>my
</code></pre>
<h3>1.2.8. if <strong>name</strong> == "<strong>main</strong>"</h3>
<p>在自定义模块中，经常会看到下面这种写法：</p>
<pre><code>if __name__ == "__main__":
    # 测试代码
    pass
</code></pre>
<p>它的作用是：</p>
<blockquote>
<p>只有当前文件被直接运行时，下面的代码才会执行；如果当前文件被其他模块导入，下面的代码不会执行。</p>
</blockquote>
<p>示例：</p>
<pre><code>def circle_area(r):
    return 3.14 * r * r

def circle_perimeter(r):
    return 2 * 3.14 * r

if __name__ == "__main__":
    print(circle_area(10))
    print(circle_perimeter(10))
</code></pre>
<p>这样写的好处是：</p>
<ol>
<li>可以在模块内部编写测试代码。</li>
<li>测试代码不会在模块被导入时自动执行。</li>
<li>更适合代码复用和项目开发。</li>
</ol>
<h3>1.2.9. 自定义模块小结</h3>
<p>自定义模块的核心要点如下：</p>
<ol>
<li>一个 <code>.py</code> 文件就是一个模块。</li>
<li>模块名就是文件名，不包含 <code>.py</code> 后缀。</li>
<li>模块中可以定义变量、函数、类和可执行代码。</li>
<li>使用模块前需要先导入。</li>
<li>自定义模块可以提高代码复用性和项目可维护性。</li>
<li><code>__all__</code> 用于控制 <code>from 模块名 import *</code> 时导入哪些功能。</li>
<li><code>__name__</code> 用于判断模块是被直接运行，还是被其他模块导入。</li>
</ol>
<h2>1.3. 软件包（package）</h2>
<h3>1.3.1. 为什么需要软件包</h3>
<p>当项目规模变大时，模块文件会越来越多。</p>
<p>例如，一个项目中可能有很多模块：</p>
<pre><code>data_query.py
data_handle.py
data_model.py
data_analysis.py
web_routes.py
web_auth.py
helpers.py
file.py
format.py
token.py
log.py
web_service.py
</code></pre>
<p>如果所有模块都放在同一个目录下，就容易造成混乱，不方便管理和维护。</p>
<p>这时就可以使用软件包（package）来组织模块。</p>
<h3>1.3.2. 什么是包</h3>
<p>包的本质是一个文件夹。</p>
<p>这个文件夹中可以包含多个 Python 模块，也就是多个 <code>.py</code> 文件。</p>
<p>同时，包文件夹下通常还会包含一个特殊文件：</p>
<pre><code>__init__.py
</code></pre>
<p>因此，可以简单理解为：</p>
<blockquote>
<p>包就是用来管理多个模块的文件夹。</p>
</blockquote>
<p>例如：</p>
<pre><code>utils/
    __init__.py
    my_var.py
    my_config.py
    my_fun.py
</code></pre>
<p>其中：</p>
<table>
<thead>
<tr>
<th>文件或目录</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>utils/</code></td>
<td>包名</td>
</tr>
<tr>
<td><code>__init__.py</code></td>
<td>标识当前文件夹是一个包</td>
</tr>
<tr>
<td><code>my_var.py</code></td>
<td>包中的模块</td>
</tr>
<tr>
<td><code>my_config.py</code></td>
<td>包中的模块</td>
</tr>
<tr>
<td><code>my_fun.py</code></td>
<td>包中的模块</td>
</tr>
</tbody>
</table>
<h3>1.3.3. 包的作用</h3>
<p>包的主要作用是：</p>
<ol>
<li>管理多个模块。</li>
<li>对模块进行分类。</li>
<li>让项目结构更加清晰。</li>
<li>提高代码维护性。</li>
<li>避免模块过多造成混乱。</li>
</ol>
<p>例如，可以按照功能把模块放入不同包中：</p>
<pre><code>project/
    data/
        __init__.py
        data_query.py
        data_handle.py
        data_model.py

    web/
        __init__.py
        web_routes.py
        web_auth.py
        web_service.py

    utils/
        __init__.py
        file.py
        format.py
        log.py
</code></pre>
<p>这样就能清楚地区分数据处理、网页服务和工具函数等不同功能。</p>
<h3>1.3.4. <strong>init</strong>.py 的作用</h3>
<p><code>__init__.py</code> 是包中的特殊文件。</p>
<p>它的主要作用包括：</p>
<ol>
<li>标识当前文件夹是一个 Python 包。</li>
<li>可以描述当前包的信息。</li>
<li>可以编写包初始化代码。</li>
<li>可以通过 <code>__all__</code> 控制 <code>from 包名 import *</code> 时允许导入的模块列表。</li>
</ol>
<p>例如，在 <code>utils/__init__.py</code> 中写入：</p>
<pre><code>__all__ = ["my_fun", "my_var"]
</code></pre>
<p>表示当执行下面代码时：</p>
<pre><code>from utils import *
</code></pre>
<p>只会导入：</p>
<pre><code>my_fun
my_var
</code></pre>
<p>不会导入其他未写入 <code>__all__</code> 的模块。</p>
<h3>1.3.5. 包的导入方式1：import 包名.模块名</h3>
<p>语法：</p>
<pre><code>import 包名.模块名
</code></pre>
<p>示例：</p>
<pre><code>import utils.my_fun

utils.my_fun.log_separator1()
</code></pre>
<p>说明：</p>
<ul>
<li><code>utils</code> 是包名。</li>
<li><code>my_fun</code> 是包中的模块名。</li>
<li>调用功能时，需要写完整路径：<code>包名.模块名.功能名</code>。</li>
</ul>
<h3>1.3.6. 包的导入方式2：from 包名 import 模块名</h3>
<p>语法：</p>
<pre><code>from 包名 import 模块名
</code></pre>
<p>示例：</p>
<pre><code>from utils import my_fun

my_fun.log_separator1()
</code></pre>
<p>说明：</p>
<ul>
<li>从 <code>utils</code> 包中导入 <code>my_fun</code> 模块。</li>
<li>调用时可以直接使用 <code>模块名.功能名</code>。</li>
</ul>
<h3>1.3.7. 包的导入方式3：from 包名 import *</h3>
<p>语法：</p>
<pre><code>from 包名 import *
</code></pre>
<p>示例：</p>
<pre><code>from utils import *

my_fun.log_separator1()
</code></pre>
<p>注意：</p>
<blockquote>
<p>使用 <code>from 包名 import *</code> 时，需要在包的 <code>__init__.py</code> 文件中添加 <code>__all__</code>，控制允许导入的模块列表。</p>
</blockquote>
<p>例如：</p>
<pre><code>__all__ = ["my_fun", "my_var"]
</code></pre>
<h3>1.3.8. 包的导入方式4：from 包名.模块名 import 功能名</h3>
<p>语法：</p>
<pre><code>from 包名.模块名 import 功能名
</code></pre>
<p>示例：</p>
<pre><code>from utils.my_fun import log_separator1

log_separator1()
</code></pre>
<p>说明：</p>
<ul>
<li>直接从包中的某个模块导入指定功能。</li>
<li>使用时可以直接调用功能名。</li>
</ul>
<h3>1.3.9. 包的导入方式5：from 包名.模块名 import *</h3>
<p>语法：</p>
<pre><code>from 包名.模块名 import *
</code></pre>
<p>示例：</p>
<pre><code>from utils.my_fun import *

log_separator1()
</code></pre>
<p>说明：</p>
<ul>
<li>从包中的某个模块导入全部允许导入的功能。</li>
<li>如果模块内部定义了 <code>__all__</code>，则只导入 <code>__all__</code> 中指定的功能。</li>
</ul>
<h3>1.3.10. 包的导入方式总结</h3>
<table>
<thead>
<tr>
<th>导入形式</th>
<th>代码样例</th>
<th>调用方式</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>import 包名.模块名</code></td>
<td><code>import utils.my_fun</code></td>
<td><code>utils.my_fun.log_separator1()</code></td>
</tr>
<tr>
<td><code>from 包名 import 模块名</code></td>
<td><code>from utils import my_fun</code></td>
<td><code>my_fun.log_separator1()</code></td>
</tr>
<tr>
<td><code>from 包名 import *</code></td>
<td><code>from utils import *</code></td>
<td><code>my_fun.log_separator1()</code></td>
</tr>
<tr>
<td><code>from 包名.模块名 import 功能名</code></td>
<td><code>from utils.my_fun import log_separator1</code></td>
<td><code>log_separator1()</code></td>
</tr>
<tr>
<td><code>from 包名.模块名 import *</code></td>
<td><code>from utils.my_fun import *</code></td>
<td><code>log_separator1()</code></td>
</tr>
</tbody>
</table>
<h3>1.3.11. 包小结</h3>
<p>关于包，需要掌握以下内容：</p>
<ol>
<li>包本质上是一个文件夹。</li>
<li>包中可以存放多个 Python 模块。</li>
<li>包通常包含一个 <code>__init__.py</code> 文件。</li>
<li><code>__init__.py</code> 可以标识这是一个包，而不是普通文件夹。</li>
<li>当模块较多时，可以用包来分类管理模块。</li>
<li><code>from 包名 import *</code> 时，可以通过 <code>__init__.py</code> 中的 <code>__all__</code> 控制导入范围。</li>
</ol>
<h1>2. 本章总结</h1>
<h2>2.1. 模块总结</h2>
<ol>
<li>一个 <code>.py</code> 文件就是一个模块。</li>
<li>模块中可以定义变量、函数、类和可执行代码。</li>
<li>使用模块可以提高代码复用性。</li>
<li>使用模块可以让项目结构更加清晰。</li>
<li>使用模块前必须先导入。</li>
<li>模块导入语句一般写在文件开头。</li>
</ol>
<h2>2.2. 模块导入方式总结</h2>
<p>常见模块导入方式如下：</p>
<pre><code>import 模块名

import 模块名 as 别名

from 模块名 import 功能名

from 模块名 import 功能名 as 别名

from 模块名 import *
</code></pre>
<p>其中：</p>
<ol>
<li><code>import 模块名</code> 需要通过 <code>模块名.功能名</code> 使用。</li>
<li><code>import 模块名 as 别名</code> 需要通过 <code>别名.功能名</code> 使用。</li>
<li><code>from 模块名 import 功能名</code> 可以直接使用功能名。</li>
<li><code>from 模块名 import *</code> 会导入模块中允许导入的全部功能，但大型项目中不建议滥用。</li>
</ol>
<h2>2.3. 自定义模块总结</h2>
<ol>
<li>自定义模块就是自己创建的 <code>.py</code> 文件。</li>
<li>模块名就是文件名，不包含 <code>.py</code> 后缀。</li>
<li>自定义模块可以把复杂项目拆分成多个文件。</li>
<li><code>__all__</code> 可以控制 <code>from 模块名 import *</code> 的导入范围。</li>
<li><code>__name__</code> 可以判断模块是直接运行还是被导入。</li>
<li><code>if __name__ == "__main__"</code> 常用于编写模块测试代码。</li>
</ol>
<h2>2.4. 包总结</h2>
<ol>
<li>包本质上是一个文件夹。</li>
<li>包中可以存放多个模块。</li>
<li>包通常包含 <code>__init__.py</code> 文件。</li>
<li>包可以对多个模块进行归类管理。</li>
<li>包的本质也是模块。</li>
<li>当项目模块较多时，使用包可以让项目结构更加清晰。</li>
</ol>
<h2>2.5. 包导入方式总结</h2>
<p>常见包导入方式如下：</p>
<pre><code>import 包名.模块名

from 包名 import 模块名

from 包名 import *

from 包名.模块名 import 功能名

from 包名.模块名 import *
</code></pre>
<p>使用建议：</p>
<ol>
<li>项目较小时，可以直接使用模块。</li>
<li>项目变大后，建议按照功能拆分模块。</li>
<li>模块数量较多时，建议使用包进行分类管理。</li>
<li>尽量少用 <code>import *</code>，避免命名冲突。</li>
<li>优先选择结构清晰、来源明确的导入方式。</li>
</ol>
]]></content>
    <author><name>Hygen</name></author>
  </entry>
  <entry>
    <title>Python核心语法06-面向对象基础</title>
    <link href="https://www.hygen.red/posts/pylearning/001_python%E6%A0%B8%E5%BF%83%E8%AF%AD%E6%B3%95/006%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E5%9F%BA%E7%A1%80/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/pylearning/001_python%E6%A0%B8%E5%BF%83%E8%AF%AD%E6%B3%95/006%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E5%9F%BA%E7%A1%80/</id>
    <published>2026-05-24T00:00:00.000Z</published>
    <updated>2026-05-24T00:00:00.000Z</updated>
    <summary>Python面向对象基础学习</summary>
    <content type="html"><![CDATA[<h1>1. 面向对象概述</h1>
<h2>1.1. 面向过程与面向对象</h2>
<h3>1.1.1. 面向过程编程</h3>
<p><strong>核心思想</strong>：把一个需求分解成一系列要执行的步骤，然后按照步骤依次执行这些任务（关注的是流程、步骤）。</p>
<p><strong>适用场景</strong>：面向过程编程非常直接，适合简单、线性的任务。</p>
<p>例如，盖房子的面向过程思路：</p>
<ol>
<li>平整地基</li>
<li>地基打桩</li>
<li>地基浇筑</li>
<li>主体施工</li>
<li>砌墙施工</li>
<li>外墙施工</li>
<li>室内硬装</li>
<li>室内软装</li>
</ol>
<h3>1.1.2. 面向对象编程</h3>
<p><strong>核心思想</strong>：把一个人/物的特征和功能打包到一起，关注的是"谁来帮我做这件事儿"。</p>
<p><strong>对象</strong>：可以理解为现实中具体的人/物在程序中的数字化身（万物皆对象）。</p>
<p>每个对象都包含两部分：</p>
<ul>
<li><strong>属性</strong>：对象的特征（如颜色、油耗、马力、工种、年龄）</li>
<li><strong>方法</strong>：对象的功能/行为（如推平、挖掘、打桩、砌墙）</li>
</ul>
<p>例如，盖房子的面向对象思路：</p>
<ul>
<li>挖掘机对象：负责平整地基</li>
<li>打桩机对象：负责地基打桩</li>
<li>混凝土车对象：负责地基浇筑</li>
<li>工人对象：负责砌墙、装修等工作</li>
</ul>
<h2>1.2. 类与对象</h2>
<h3>1.2.1. 什么是类</h3>
<p><strong>类</strong>：描述的是一组具有相同属性（特征）和方法（功能/行为）的模板。</p>
<h3>1.2.2. 什么是对象</h3>
<p><strong>对象</strong>：对象是类的实例，是基于类创建出来的（实例对象）。</p>
<p><strong>类与对象的关系</strong>：</p>
<ul>
<li>类是模板，对象是根据模板创建出来的具体实例</li>
<li>一个类可以创建无数个对象</li>
<li>创建对象的过程，也称为对象的实例化</li>
</ul>
<p>就像月饼模具和月饼的关系：</p>
<ul>
<li>月饼模具是类，定义了月饼的形状、花纹等特征</li>
<li>每个月饼是对象，具有模具定义的特征，但可以有不同的颜色、口味等属性</li>
</ul>
<h1>2. 类与对象的定义</h1>
<h2>2.1. 基本类定义</h2>
<h3>2.1.1. 类定义语法</h3>
<pre><code># 定义类
class 类名:
    pass

# 创建对象
对象名 = 类名()
</code></pre>
<p><strong>命名规范</strong>：类名遵循大驼峰命名法，每个单词的首字母都是大写，单词之间没有分隔符，例如：<code>UserInfo</code>、<code>UserAccount</code>、<code>Car</code>。</p>
<h3>2.1.2. 动态添加属性</h3>
<p>创建对象后，可以动态地为对象添加属性：</p>
<pre><code># 定义类
class Car:
    pass

# 创建对象
c1 = Car()

# 动态添加属性
c1.brand = "BMW"
c1.name = "X5"
c1.price = 500000

# 查看对象的所有属性
print(c1.__dict__)  # 输出: {'brand': 'BMW', 'name': 'X5', 'price': 500000}
</code></pre>
<p><strong>说明</strong>：<code>__dict__</code>是Python中用户自定义类实例的一个特殊属性，用于以字典形式存储对象的属性。</p>
<h2>2.2. 初始化方法<code>__init__</code></h2>
<h3>2.2.1. <code>__init__</code>方法介绍</h3>
<p><code>__init__</code>是初始化方法，对象创建后自动调用，主要用于设置对象的初始状态（设置对象属性）。</p>
<h3>2.2.2. <code>__init__</code>方法语法</h3>
<pre><code># 定义类
class 类名:
    def __init__(self, 参数列表):
       self.属性名 = 参数值
       self.属性名 = 参数值

# 创建对象
对象名 = 类名(参数列表)
</code></pre>
<h3>2.2.3. <code>self</code>参数介绍</h3>
<p><code>self</code>是类中定义的方法的第一个参数，表示当前创建的实例对象。</p>
<p>在方法内部，通过<code>self.属性名</code>可以访问和修改对象的属性。</p>
<h3>2.2.4. 示例</h3>
<pre><code># 定义类
class Car:
    def __init__(self, c_brand, c_name, c_price):
        self.brand = c_brand
        self.name = c_name
        self.price = c_price

# 创建对象
c1 = Car("BMW", "X5", 500000)

# 查看对象的所有属性
print(c1.__dict__)  # 输出: {'brand': 'BMW', 'name': 'X5', 'price': 500000}

# 访问对象属性
print(c1.brand)  # 输出: BMW
print(c1.name)  # 输出: X5
print(c1.price)  # 输出: 500000
</code></pre>
<p><strong>推荐</strong>：在定义类时，尽量使用<code>__init__</code>方法来初始化对象的属性，而不是动态添加属性。</p>
<h2>2.3. 小结</h2>
<ol>
<li>定义类时，类名遵循<strong>大驼峰命名法</strong>。</li>
<li><code>__init__</code>是初始化方法，对象创建时自动调用，主要用于设置对象的初始状态。</li>
<li><code>self</code>是类中定义的方法的第一个参数，表示当前创建的实例对象。</li>
</ol>
<h1>3. 实例方法</h1>
<h2>3.1. 实例方法定义</h2>
<p>定义在类中的函数称之为方法，实例方法是属于每个实例对象的方法。</p>
<h3>3.1.1. 实例方法语法</h3>
<pre><code># 定义类
class 类名:
    def __init__(self, 形参列表):
       self.属性名 = 参数值
       self.属性名 = 参数值
    
    def 方法名(self, 形参列表):
       方法体
</code></pre>
<h3>3.1.2. 实例方法调用</h3>
<pre><code>对象名.方法名(实参)
</code></pre>
<p><strong>注意</strong>：调用实例方法时，不需要传递<code>self</code>参数，Python会自动将当前对象传递给<code>self</code>。</p>
<h2>3.2. 示例</h2>
<pre><code>class Car:
    def __init__(self, brand, name, price):
        self.brand = brand
        self.name = name
        self.price = price
    
    def running(self):
        """汽车行驶方法"""
        print(f"{self.brand} {self.name} 正在高速行驶...")
    
    def total_cost(self, discount, rate):
        """计算提车总价"""
        return self.price * discount + self.price * rate

# 创建对象
c1 = Car("BMW", "X5", 500000)

# 调用实例方法
c1.running()  # 输出: BMW X5 正在高速行驶...

total_price = c1.total_cost(0.9, 0.1)
print(f"提车总价为: {total_price:.0f}")  # 输出: 提车总价为: 500000
</code></pre>
<h1>4. 魔法方法</h1>
<h2>4.1. 魔法方法介绍</h2>
<p><strong>魔法方法</strong>是指Python中提供的以双下划线开头和结尾的特殊方法，用于定义类的特殊行为。</p>
<p><strong>特点</strong>：魔法方法不需要我们手动调用，Python会在合适的时机自动调用。</p>
<h2>4.2. 常用魔法方法</h2>
<table>
<thead>
<tr>
<th>魔法方法</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>__init__</code></td>
<td>初始化方法，对象创建时自动调用</td>
</tr>
<tr>
<td><code>__str__</code></td>
<td>字符串表示方法，使用<code>print()</code>打印对象时自动调用</td>
</tr>
<tr>
<td><code>__eq__</code></td>
<td>相等比较方法，使用<code>==</code>比较两个对象时自动调用</td>
</tr>
<tr>
<td><code>__lt__</code></td>
<td>小于比较方法，使用<code>&lt;</code>比较两个对象时自动调用</td>
</tr>
<tr>
<td><code>__le__</code></td>
<td>小于等于比较方法，使用<code>&lt;=</code>比较两个对象时自动调用</td>
</tr>
<tr>
<td><code>__gt__</code></td>
<td>大于比较方法，使用<code>&gt;</code>比较两个对象时自动调用</td>
</tr>
<tr>
<td><code>__ge__</code></td>
<td>大于等于比较方法，使用<code>&gt;=</code>比较两个对象时自动调用</td>
</tr>
</tbody>
</table>
<h2>4.3. 魔法方法示例</h2>
<h3>4.3.1. <code>__str__</code>方法</h3>
<p>默认情况下，打印对象会输出对象的内存地址。通过重写<code>__str__</code>方法，可以自定义对象的字符串表示。</p>
<pre><code>class Car:
    def __init__(self, brand, name, price):
        self.brand = brand
        self.name = name
        self.price = price
    
    def __str__(self):
        return f"{self.brand} {self.name} 价格: {self.price}元"

c1 = Car("BMW", "X5", 500000)
print(c1)  # 输出: BMW X5 价格: 500000元
</code></pre>
<h3>4.3.2. <code>__eq__</code>方法</h3>
<p>默认情况下，使用<code>==</code>比较两个对象会比较它们的内存地址。通过重写<code>__eq__</code>方法，可以自定义对象的相等比较逻辑。</p>
<pre><code>class Car:
    def __init__(self, brand, name, price):
        self.brand = brand
        self.name = name
        self.price = price
    
    def __eq__(self, other):
        # 当品牌、名称和价格都相同时，认为两个对象相等
        return (self.price == other.price 
                and self.brand == other.brand 
                and self.name == other.name)

c1 = Car("BMW", "X5", 500000)
c2 = Car("BMW", "X5", 500000)
c3 = Car("Audi", "Q7", 600000)

print(c1 == c2)  # 输出: True
print(c1 == c3)  # 输出: False
</code></pre>
<h3>4.3.3. <code>__lt__</code>方法</h3>
<p>默认情况下，自定义对象之间不能进行大小比较。通过重写<code>__lt__</code>方法，可以自定义对象的小于比较逻辑。</p>
<pre><code>class Car:
    def __init__(self, brand, name, price):
        self.brand = brand
        self.name = name
        self.price = price
    
    def __lt__(self, other):
        # 按价格比较大小
        return self.price &lt; other.price

c1 = Car("BMW", "X5", 500000)
c2 = Car("Audi", "Q7", 600000)

print(c1 &lt; c2)  # 输出: True
print(c1 &gt; c2)  # 输出: False
</code></pre>
<h2>4.4. 完整示例</h2>
<pre><code>class Car:
    def __init__(self, brand, name, price):
        self.brand = brand
        self.name = name
        self.price = price
    
    def running(self):
        print(f"{self.brand} {self.name} 正在高速行驶...")
    
    def __str__(self):
        return f"{self.brand} {self.name} {self.price}"
    
    def __eq__(self, other):
        return self.price == other.price and self.brand == other.brand and self.name == other.name
    
    def __lt__(self, other):
        return self.price &lt; other.price

c1 = Car("BMW", "X5", 500000)
print(c1)  # 输出: BMW X5 500000

c2 = Car("BMW", "X5", 500000)
print(c2)  # 输出: BMW X5 500000

print(c1 == c2)  # 输出: True
print(c1 &lt; c2)  # 输出: False
</code></pre>
<h2>4.5. 小结</h2>
<ol>
<li>魔法方法是Python中提供的<code>__xxx__</code>形式的特殊方法。</li>
<li>魔法方法无需手动调用，Python会在合适的时机自动调用。</li>
<li>常用的魔法方法有：
<ul>
<li><code>__init__</code>：初始化对象</li>
<li><code>__str__</code>：自定义对象的字符串表示</li>
<li><code>__eq__</code>：自定义对象的相等比较逻辑</li>
<li><code>__lt__</code>、<code>__le__</code>、<code>__gt__</code>、<code>__ge__</code>：自定义对象的大小比较逻辑</li>
</ul>
</li>
</ol>
<h1>5. 实例属性与类属性</h1>
<h2>5.1. 属性分类</h2>
<p>属性分为两类：</p>
<ul>
<li><strong>实例属性</strong>：属于每个具体对象的属性，每个对象都是独立的（各个对象特有的数据）。</li>
<li><strong>类属性</strong>：属于类本身的属性，所有实例共享（所有对象共享的数据或配置）。</li>
</ul>
<h2>5.2. 实例属性</h2>
<p>实例属性在<code>__init__</code>方法中定义，通过<code>self.属性名</code>访问。</p>
<p>每个对象的实例属性都是独立的，修改一个对象的实例属性不会影响其他对象。</p>
<h2>5.3. 类属性</h2>
<p>类属性在类的内部、方法的外部定义，通过<code>类名.属性名</code>访问。</p>
<p>所有对象共享类属性，修改类属性会影响所有对象。</p>
<h2>5.4. 示例</h2>
<pre><code>class Car:
    # 类属性
    wheel = 4  # 轮胎数量
    tax_rate = 0.1  # 购置税税率
    
    def __init__(self, c_brand, c_name, c_price):
        # 实例属性
        self.brand = c_brand
        self.name = c_name
        self.price = c_price
    
    def running(self):
        print(f"{self.brand} {self.name} 正在高速行驶...")
    
    def total_cost(self):
        """计算提车总价"""
        return self.price + self.price * Car.tax_rate

# 创建两个对象
c1 = Car("BYD", "汉", 180000)
c2 = Car("Tesla", "Model Y", 260000)

# 访问实例属性
print(c1.brand)  # 输出: BYD
print(c2.brand)  # 输出: Tesla

# 访问类属性
print(Car.wheel)  # 输出: 4
print(c1.wheel)  # 输出: 4
print(c2.wheel)  # 输出: 4

# 修改类属性
Car.tax_rate = 0.08

# 所有对象都会受到影响
print(c1.total_cost())  # 输出: 194400.0
print(c2.total_cost())  # 输出: 280800.0
</code></pre>
<p><strong>说明</strong>：通过实例查找属性时，会先查找实例属性，实例属性不存在时，再查找类属性。</p>
<h1>6. 案例实操：教务管理系统</h1>
<h2>6.1. 需求分析</h2>
<p>采用面向对象的编程思想，完成教务管理系统的开发。教务管理系统可以管理在校学生的成绩信息，通过控制台菜单与用户交互，具体功能如下：</p>
<ol>
<li>添加学生成绩：根据输入的学生姓名、语文成绩、数学成绩、英语成绩，记录在系统中</li>
<li>修改学生成绩：根据输入的学生姓名，修改对应的学生成绩</li>
<li>删除学生成绩：根据输入的学生姓名，删除对应的学生成绩</li>
<li>查询指定学生成绩：根据输入的学生姓名，查找对应的学生成绩，并输出</li>
<li>展示全部学生成绩：展示出系统中所有学生的成绩</li>
</ol>
<h2>6.2. 类设计</h2>
<ul>
<li><code>Student</code>类：表示学生，包含学生的姓名、语文成绩、数学成绩、英语成绩等属性，以及计算总分、平均分等方法</li>
<li><code>EduManagement</code>类：表示教务管理系统，包含学生列表属性，以及添加、修改、删除、查询、展示学生成绩等方法</li>
</ul>
<h2>6.3. 代码实现</h2>
<pre><code>class Student:
    """学生类"""
    def __init__(self, name, chinese, math, english):
        self.name = name
        self.chinese = chinese
        self.math = math
        self.english = english
    
    def total_score(self):
        """计算总分"""
        return self.chinese + self.math + self.english
    
    def avg_score(self):
        """计算平均分"""
        return self.total_score() / 3
    
    def __str__(self):
        return (f"姓名: {self.name}, 语文: {self.chinese}, 数学: {self.math}, "
                f"英语: {self.english}, 总分: {self.total_score()}, 平均分: {self.avg_score():.1f}")

class EduManagement:
    """教务管理系统类"""
    def __init__(self):
        self.students = {}  # 学生字典，key是学生姓名，value是Student对象
    
    def add_student(self, student):
        """添加学生"""
        if student.name in self.students:
            print(f"学生{student.name}已存在！")
            return False
        self.students[student.name] = student
        print(f"学生{student.name}添加成功！")
        return True
    
    def update_student(self, name, chinese, math, english):
        """修改学生成绩"""
        if name not in self.students:
            print(f"学生{name}不存在！")
            return False
        student = self.students[name]
        student.chinese = chinese
        student.math = math
        student.english = english
        print(f"学生{name}成绩修改成功！")
        return True
    
    def delete_student(self, name):
        """删除学生"""
        if name not in self.students:
            print(f"学生{name}不存在！")
            return False
        del self.students[name]
        print(f"学生{name}删除成功！")
        return True
    
    def query_student(self, name):
        """查询学生成绩"""
        if name not in self.students:
            print(f"学生{name}不存在！")
            return None
        return self.students[name]
    
    def show_all_students(self):
        """展示所有学生成绩"""
        if not self.students:
            print("系统中没有学生信息！")
            return
        print("=" * 70)
        print("所有学生成绩信息：")
        print("-" * 70)
        for student in self.students.values():
            print(student)
        print("=" * 70)

def main():
    """主函数"""
    edu_system = EduManagement()
    
    while True:
        print("\n" + "=" * 30)
        print("欢迎使用教务管理系统")
        print("1. 添加学生成绩")
        print("2. 修改学生成绩")
        print("3. 删除学生成绩")
        print("4. 查询指定学生成绩")
        print("5. 展示全部学生成绩")
        print("6. 退出系统")
        print("=" * 30)
        
        choice = input("请选择要执行的操作(1-6): ")
        
        if choice == "1":
            # 添加学生成绩
            name = input("请输入学生姓名: ")
            try:
                chinese = int(input("请输入语文成绩: "))
                math = int(input("请输入数学成绩: "))
                english = int(input("请输入英语成绩: "))
            except ValueError:
                print("成绩必须是数字！")
                continue
            
            student = Student(name, chinese, math, english)
            edu_system.add_student(student)
        
        elif choice == "2":
            # 修改学生成绩
            name = input("请输入要修改的学生姓名: ")
            try:
                chinese = int(input("请输入新的语文成绩: "))
                math = int(input("请输入新的数学成绩: "))
                english = int(input("请输入新的英语成绩: "))
            except ValueError:
                print("成绩必须是数字！")
                continue
            
            edu_system.update_student(name, chinese, math, english)
        
        elif choice == "3":
            # 删除学生成绩
            name = input("请输入要删除的学生姓名: ")
            edu_system.delete_student(name)
        
        elif choice == "4":
            # 查询指定学生成绩
            name = input("请输入要查询的学生姓名: ")
            student = edu_system.query_student(name)
            if student:
                print("-" * 50)
                print(student)
                print("-" * 50)
        
        elif choice == "5":
            # 展示全部学生成绩
            edu_system.show_all_students()
        
        elif choice == "6":
            # 退出系统
            print("感谢使用教务管理系统，再见！")
            break
        
        else:
            print("输入错误，请重新选择！")

if __name__ == "__main__":
    main()
</code></pre>
<h1>7. 异常处理</h1>
<h2>7.1. 什么是异常</h2>
<p><strong>异常</strong>（也称为Bug）就是程序运行过程中出现的错误，它会中断程序的正常执行流程。</p>
<p><strong>常见异常类型</strong>：</p>
<ul>
<li><code>NameError</code>：变量未定义</li>
<li><code>TypeError</code>：类型错误</li>
<li><code>IndexError</code>：索引超出范围</li>
<li><code>KeyError</code>：字典中不存在指定的key</li>
<li><code>ValueError</code>：值错误</li>
</ul>
<p><strong>异常的作用</strong>：</p>
<ul>
<li>保证数据、逻辑的正确性，避免程序执行混乱</li>
<li>在开发阶段，尽量发现更多的问题，尽早解决问题，保障程序正常执行</li>
</ul>
<p><strong>注意</strong>：异常不是坏东西，而是编写健壮程序的重要工具。</p>
<h2>7.2. 异常处理</h2>
<p>程序运行过程中出现异常，有两种处理方案：</p>
<ol>
<li>不做处理：整个程序因为一个Bug，中断执行</li>
<li>捕获异常：按照我们自己的处理方式，处理完异常，程序继续执行</li>
</ol>
<h3>7.2.1. 异常处理语法</h3>
<pre><code>try:
    可能出现异常的业务代码1
    可能出现异常的业务代码2
    ...
except [异常类型 as 变量名]:
    出现异常时的预案
[finally:
    不管是否出现异常，都会执行的代码]
</code></pre>
<h3>7.2.2. 基本示例</h3>
<pre><code>try:
    print("=" * 30)
    print(my_name)  # my_name未定义，会抛出NameError
    print("=" * 30)
except NameError as e:
    print("程序运行报错，错误信息: ", e)
finally:
    print("释放资源 ~")
</code></pre>
<p>输出结果：</p>
<pre><code>==============================
程序运行报错，错误信息:  name 'my_name' is not defined
释放资源 ~
</code></pre>
<h3>7.2.3. 捕获多个异常</h3>
<pre><code>try:
    num = int(input("请输入一个数字: "))
    result = 10 / num
    print(f"10 / {num} = {result}")
except ValueError as e:
    print("输入错误，必须输入数字！")
except ZeroDivisionError as e:
    print("除数不能为0！")
except Exception as e:
    print("未知错误，具体信息: ", e)
finally:
    print("程序执行完毕")
</code></pre>
<h2>7.3. 异常的传递</h2>
<p><strong>异常传递</strong>就是异常在函数调用中层层上报的过程，直到有人处理它，或者程序崩溃。</p>
<pre><code>def fun1():
    print("fun1 ... running ...")
    fun2()

def fun2():
    print("fun2 ... running ...")
    fun3()

def fun3():
    print("fun3 ... running ...")
    print(my_color)  # my_color未定义，会抛出NameError

if __name__ == '__main__':
    try:
        fun1()
    except NameError as e:
        print("捕获到异常: ", e)
</code></pre>
<p>输出结果：</p>
<pre><code>fun1 ... running ...
fun2 ... running ...
fun3 ... running ...
捕获到异常:  name 'my_color' is not defined
</code></pre>
<p><strong>说明</strong>：异常从<code>fun3</code>抛出，传递到<code>fun2</code>，再传递到<code>fun1</code>，最后在主函数中被捕获处理。</p>
<h1>8. 本章总结</h1>
<h2>8.1. 面向对象核心概念</h2>
<ol>
<li><strong>对象</strong>：现实中具体的人/物在程序中的数字化身，包含属性和方法。</li>
<li><strong>类</strong>：描述一组具有相同属性和方法的对象的模板。</li>
<li><strong>实例化</strong>：根据类创建对象的过程。</li>
</ol>
<h2>8.2. 类与对象</h2>
<ol>
<li>使用<code>class</code>关键字定义类，类名遵循大驼峰命名法。</li>
<li><code>__init__</code>方法是初始化方法，对象创建时自动调用，用于设置对象的初始属性。</li>
<li><code>self</code>参数表示当前实例对象，在方法内部通过<code>self</code>访问对象的属性和方法。</li>
</ol>
<h2>8.3. 实例方法</h2>
<ol>
<li>定义在类中的函数称为方法，实例方法属于每个实例对象。</li>
<li>调用实例方法时，不需要传递<code>self</code>参数，Python会自动传递。</li>
</ol>
<h2>8.4. 魔法方法</h2>
<ol>
<li>魔法方法是Python中提供的<code>__xxx__</code>形式的特殊方法。</li>
<li>魔法方法无需手动调用，Python会在合适的时机自动调用。</li>
<li>常用魔法方法：<code>__init__</code>、<code>__str__</code>、<code>__eq__</code>、<code>__lt__</code>等。</li>
</ol>
<h2>8.5. 属性</h2>
<ol>
<li><strong>实例属性</strong>：属于每个具体对象的属性，每个对象独立拥有。</li>
<li><strong>类属性</strong>：属于类本身的属性，所有实例共享。</li>
</ol>
<h2>8.6. 异常处理</h2>
<ol>
<li>异常是程序运行过程中出现的错误，会中断程序的正常执行。</li>
<li>使用<code>try-except-finally</code>语句捕获和处理异常。</li>
<li>异常会在函数调用中层层传递，直到被处理或程序崩溃。</li>
</ol>
]]></content>
    <author><name>Hygen</name></author>
  </entry>
  <entry>
    <title>Python核心语法02-流程控制语句</title>
    <link href="https://www.hygen.red/posts/pylearning/001_python%E6%A0%B8%E5%BF%83%E8%AF%AD%E6%B3%95/002%E6%B5%81%E7%A8%8B%E6%8E%A7%E5%88%B6%E8%AF%AD%E5%8F%A5/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/pylearning/001_python%E6%A0%B8%E5%BF%83%E8%AF%AD%E6%B3%95/002%E6%B5%81%E7%A8%8B%E6%8E%A7%E5%88%B6%E8%AF%AD%E5%8F%A5/</id>
    <published>2026-05-23T00:00:00.000Z</published>
    <updated>2026-05-23T00:00:00.000Z</updated>
    <summary>Python语法学习</summary>
    <content type="html"><![CDATA[<h1>1. 条件判断</h1>
<p>在Python中，代码默认是从上往下逐行执行的。但在实际场景中，我们经常需要根据不同的条件来决定是否执行某段代码，这就需要用到条件判断语句。</p>
<h2>1.1. if语句基本格式</h2>
<p><code>if</code> 语句用于条件判断：只有满足指定条件（判断结果为 <code>True</code>），才会执行对应的代码逻辑。</p>
<h3>1.1.1. 语法格式</h3>
<pre><code>if 要判断的条件:
    条件成立时，要执行对应的操作1
    条件成立时，要执行对应的操作2
</code></pre>
<h3>1.1.2. 注意事项</h3>
<ul>
<li>判断条件的结果一定要是<strong>布尔类型</strong>。</li>
<li>不要忘记判断条件后的<strong>冒号 (:)</strong>。</li>
<li><code>if</code> 语句里面的代码块，需要在前方缩进空格（建议4个空格），通过缩进来描述代码的层级关系（归属）。</li>
</ul>
<h3>1.1.3. 案例演示</h3>
<pre><code># 定义变量
score = 695

# 条件判断
if score &gt; 680:
    print("欢迎你, 来清华读书")
</code></pre>
<h2>1.2. if...else语句</h2>
<p>如果条件成立执行操作1，否则执行操作2。</p>
<h3>1.2.1. 语法格式</h3>
<pre><code>if 要判断的条件:
    条件成立时，执行对应的操作1
else:
    条件不成立时，执行的操作2
</code></pre>
<h3>1.2.2. 注意事项</h3>
<ul>
<li><code>else</code> 是不需要条件判断的，当 <code>if</code> 条件不成立时，<code>else</code> 就执行了。</li>
<li><code>else</code> 代码块，也需要使用空格缩进（建议4个空格）。</li>
</ul>
<h3>1.2.3. 案例演示</h3>
<pre><code>account = input("请输入B站账号：")
password = input("请输入B站密码：")

if account == "18888888888" and password == "666888":
    print("登录成功 ~")
    print("进入B站首页 ~")
else:
    print("登录失败 !")
    print("用户名或密码错误 !")
</code></pre>
<h2>1.3. if...elif...else语句</h2>
<p><code>if...elif...else</code> 语句的作用是可以完成多个条件的判断(多分支判断)。</p>
<h3>1.3.1. 语法格式</h3>
<pre><code>if 要判断的条件1:
    条件成立时，执行对应的操作1
elif 要判断的条件2:
    条件成立时，执行对应的操作2
else:
    条件不成立时，执行的操作3
</code></pre>
<h3>1.3.2. 注意事项</h3>
<ul>
<li><code>elif</code> 可以写多个。</li>
<li><code>else</code> 可有可无，如果有必须放在最后。</li>
<li>多个条件判断是有顺序的，从上到下判断，如果前面有条件成立了，后面的条件不会判断了。</li>
</ul>
<h3>1.3.3. 案例演示</h3>
<pre><code>num = int(input("请输入数字："))

if num &gt; 0:
    print(f"{num} 是 正数")
elif num &lt; 0:
    print(f"{num} 是 负数")
else:
    print(f"{num} 是 0")
</code></pre>
<h2>1.4. 案例实操</h2>
<h3>1.4.1. 案例1</h3>
<p>三角形类型判断：根据输入的三个边的边长(正整数)，判定是等边三角形、等腰三角形、普通三角形，还是不能构成三角形。</p>
<p>构成三角形的条件：两边之和大于第三边</p>
<p>三角形判定规则：</p>
<ul>
<li>三个边都相等: 等边三角形</li>
<li>两个边相等: 等腰三角形</li>
<li>三个边都不相等: 普通三角形</li>
</ul>
<pre><code>a, b, c = list(map(int, input().split()))

if a + b &lt;= c or a + c &lt;= b or b + c &lt;= a:
    print("无法构成三角形")
else: # 可以构成三角形
    if a == b == c:
        print("等边三角形")
    elif a == b or b == c or a == c:
        print("等腰三角形")
    else:
        print("一般三角形")
</code></pre>
<hr />
<h1>2. 模式匹配 (match...case)</h1>
<p>结构模式匹配就是用一个清晰的模板去精准的匹配数据的结构和内容，匹配成功则执行响应的操作。基于某个变量的多个固定值进行分支判断时，可以使用match模式匹配。这是3.10版本中的新语法。</p>
<h2>2.1. 语法格式</h2>
<pre><code>match 表达式:
    case 值1:
        操作1
    case 值2 if 条件表达式:
        操作2
    case 值3 | 值4 :
        操作4
    case _:
        默认操作
</code></pre>
<ul>
<li><code>|</code> 表示或的关系，匹配多个模式中的任意一个。</li>
<li><code>case _</code> 匹配其他所有情况。</li>
</ul>
<h2>2.2. 执行流程</h2>
<ol>
<li>首先计算 <code>match</code> 指定的表达式的值。</li>
<li>从上到下依次和 <code>case</code> 后面的值进行匹配，匹配正确，就执行对应语句。</li>
<li>如果前面所有的 <code>case</code> 都没有匹配上，就会执行默认的 <code>case _</code>。</li>
</ol>
<hr />
<h1>3. 循环</h1>
<p>循环就是重复的执行某件事情。分为 <code>while</code> 循环和 <code>for</code> 循环。</p>
<h2>3.1. while循环</h2>
<p><code>while</code> 循环是通过条件表达式，来控制是否要进行下一次循环。</p>
<h3>3.1.1. 语法格式</h3>
<pre><code>while 条件表达式:
    循环体语句1
    ...
else:
    条件为False,循环正常结束时执行
</code></pre>
<h3>3.1.2. 注意事项</h3>
<ul>
<li>条件表达式的结果为布尔类型。</li>
<li>通过空格缩进表述层级关系。</li>
<li>规划好循环终止的条件，否则将无限循环（死循环）。</li>
</ul>
<h2>3.2. for循环</h2>
<p><code>while</code> 循环用于在某个条件满足时一直循环，循环的次数通常是未知的。而 <code>for</code> 循环本质是一种轮询遍历机制，对一批内容进行逐个处理。用于对一个已知的数据集进行遍历或已知次数的循环。</p>
<h3>3.2.1. 语法格式</h3>
<pre><code>for 元素 in 待处理数据集:
    循环体代码(对元素进行处理)
else:
    循环结束时, 执行的代码
</code></pre>
<h2>3.3. range语句</h2>
<p><code>range</code> 语句作用是生成指定规则的数字序列。</p>
<table>
<thead>
<tr>
<th>用法</th>
<th>说明</th>
<th>示例</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>range(end)</code></td>
<td>获取一个从0开始，到end结束的数字序列（不含end本身）</td>
<td><code>range(5)</code> 获取的数据就是 <code>0,1,2,3,4</code></td>
</tr>
<tr>
<td><code>range(start, end)</code></td>
<td>获取一个从start开始，到end结束的数字序列（不含end本身）</td>
<td><code>range(2,8)</code> 获取的数据就是 <code>2,3,4,5,6,7</code></td>
</tr>
<tr>
<td><code>range(start, end, step)</code></td>
<td>获取一个从start开始，到end结束的数字序列，step步长（不含end本身）</td>
<td><code>range(0,10,2)</code> 获取的数据就是 <code>0,2,4,6,8</code></td>
</tr>
</tbody>
</table>
<h2>3.4. 嵌套循环与控制关键字</h2>
<p>嵌套循环语法格式为在外层循环结构内包含内层循环结构。</p>
<ul>
<li><strong><code>break</code> 关键字</strong>：不能够单独书写，只能出现循环中，表示结束、跳出的意思。</li>
<li><strong><code>continue</code> 关键字</strong>：不能够单独书写，只能出现循环中，表示中断本次循环，直接进入下一次循环。</li>
</ul>
<h2>3.5. 案例实操</h2>
<h3>3.5.1. 案例1</h3>
<p>将1-1000之间（含1000）所有的5的倍数的数字累加起来。</p>
<pre><code>cnt = 0
for i in range(0, 1001, 5):
  cnt += i

print(cnt) # 输出： 100500
</code></pre>
<h3>3.5.2. 案例2</h3>
<p>统计字符串 "akiwksjakdiklowiqaamnvbamvaxnsjdsjkaaxkjd" 字符串中有多少个a和k。</p>
<pre><code>s = "akiwksjakdiklowiqaamnvbamvaxnsjdsjkaaxkjd"
cnt = 0

for c in s:
  if c == 'a' or c == 'k':
    cnt += 1

print(cnt) # 输出：14
</code></pre>
]]></content>
    <author><name>Hygen</name></author>
  </entry>
  <entry>
    <title>Python核心语法01-数据存储与运算</title>
    <link href="https://www.hygen.red/posts/pylearning/001_python%E6%A0%B8%E5%BF%83%E8%AF%AD%E6%B3%95/001%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8%E4%B8%8E%E8%BF%90%E7%AE%97/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/pylearning/001_python%E6%A0%B8%E5%BF%83%E8%AF%AD%E6%B3%95/001%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8%E4%B8%8E%E8%BF%90%E7%AE%97/</id>
    <published>2026-05-22T00:00:00.000Z</published>
    <updated>2026-05-22T00:00:00.000Z</updated>
    <summary>Python语法学习</summary>
    <content type="html"><![CDATA[<h1>1. 数据存储与运算</h1>
<h2>1.1. 字面量</h2>
<h3>1.1.1. 定义</h3>
<p>程序中，直接书写的<strong>固定值（数据）</strong>，称为字面量。</p>
<p>如：18，"Hello World"等。</p>
<h3>1.1.2. 字面量类型及书写格式</h3>
<table>
<thead>
<tr>
<th>类型</th>
<th>说明</th>
<th>示例</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>数字类型</strong></td>
<td>整数(int)</td>
<td><code>10</code> <code>18</code> <code>-5</code> <code>0</code></td>
</tr>
<tr>
<td><strong>数字类型</strong></td>
<td>浮点数/小数(float)</td>
<td><code>8.5</code> <code>3.14</code> <code>1.0</code> <code>-3.5</code></td>
</tr>
<tr>
<td><strong>布尔(bool)</strong></td>
<td>表达现实生活中的逻辑，真或假</td>
<td><code>True</code> <code>False</code></td>
</tr>
<tr>
<td><strong>字符串(str)</strong></td>
<td>描述文本的一种数据类型</td>
<td><code>"人生苦短，我用Python"</code></td>
</tr>
<tr>
<td><strong>空值(NoneType)</strong></td>
<td>表示空或无值，仅包含一个值 None</td>
<td><code>None</code></td>
</tr>
<tr>
<td><strong>数据容器</strong></td>
<td>存储多项数据的容器</td>
<td>列表/元组/集合/字典（后面详细讲解）</td>
</tr>
</tbody>
</table>
<p>其中，bool类型本质就是数字类型，bool转数字时，True=1,False=0，数字转bool时，0=False，其它=True</p>
<h2>1.2. 变量</h2>
<h3>1.2.1. 定义</h3>
<p>程序中，用来存储单个数据的<strong>容器</strong>（经常会发生改变的数据），称为变量。</p>
<p>如，<code>num=1</code>中的num是变量名，1为变量值，之后<code>num=2</code>也是可以的，<code>num</code>作为存储容器，里面存储的内容可以不一样。</p>
<h3>1.2.2. 作用</h3>
<p>变量主要用于输出打印、参与计算、记录数据，是编程过程中必不可少的元素。</p>
<h3>1.2.3. 注意事项</h3>
<ol>
<li>一个变量只能存储一个值（num=1）</li>
<li>变量定义的时候必须赋值才可以使用（C++等语言中可以先声明后赋值）</li>
<li>一条语句可以定义多个变量，也可以连续赋值(a, b = 1, "Python")</li>
</ol>
<h2>1.3. 标识符</h2>
<h3>1.3.1. 定义</h3>
<p>程序员在代码中为变量、函数、类等元素所起的名字，称为标识符。</p>
<h3>1.3.2. 标识符命名规则</h3>
<ol>
<li>只能包含字母</li>
<li>数字不能开头</li>
<li>不能使用关键字（if, else, elif, for, while, and, or等）</li>
<li>严格区分大小写（Age和age不是同一个变量）</li>
</ol>
<h3>1.3.3. 变量名命名规范</h3>
<ol>
<li>顾名思义（年龄用age，姓名用name等，尽量少用a,b这种）</li>
<li>多个部分用下划线连接（Java则是驼峰）</li>
<li>英文字母全小写</li>
</ol>
<h2>1.4. 案例实操</h2>
<h3>案例1</h3>
<p>现有两个变量，分别为：a = 10， b = 20，现需要将这两个变量值交换，然后输出到控制台。</p>
<h4>代码1</h4>
<pre><code>a, b = 10, 20 # 变量名赋值
a, b = b, a # 变量名交换
print(a, b) # 输出交换后的a, b
</code></pre>
<h4>代码2</h4>
<pre><code>a, b = 10, 20 # 变量名赋值

# 通过中间变量来交换变量名
c = a
a = b
b = c

print(a, b) # 输出交换后的变量值
</code></pre>
<h3>案例2</h3>
<p>现有三个变量，分别为：a = 100，b = 200，c = 300，现需要将这三个变量值进行交换，将a,b,c的值分别赋值给c,a,b，并将其输出到控制台。</p>
<h4>代码</h4>
<pre><code>a, b, c = 100, 200, 300
a, b, c = c, a, b
print(a, b, c)
</code></pre>
<h1>2. 常见数据类型</h1>
<table>
<thead>
<tr>
<th>标准名称</th>
<th>描述</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>int</code></td>
<td>整数</td>
<td>数字类型，存放整数，如：<code>10</code> <code>-5</code></td>
</tr>
<tr>
<td><code>float</code></td>
<td>浮点数</td>
<td>数字类型，存放小数，如：<code>8.5</code> <code>3.14</code> <code>1.0</code> <code>-3.5</code></td>
</tr>
<tr>
<td><code>str</code></td>
<td>字符串</td>
<td>用引号引起来的都是字符串，如：<code>"Python"</code></td>
</tr>
<tr>
<td><code>bool</code></td>
<td>布尔</td>
<td>布尔类型，描述真和假，如：<code>True</code> <code>False</code></td>
</tr>
<tr>
<td><code>NoneType</code></td>
<td>空值</td>
<td>表示空或无值，仅包含一个值 <code>None</code></td>
</tr>
</tbody>
</table>
<h2>2.1. 数据类型常用方法</h2>
<h3>2.1.1. type()</h3>
<p>我们可以通过<code>type()</code>来获取数据的类型</p>
<pre><code>print(type(1))
</code></pre>
<p>输出:</p>
<pre><code>&lt;class 'int'&gt;
</code></pre>
<p>意思是1属于整型类，同理可以试试<code>3.14</code>, <code>"hello"</code>等数据的类型。</p>
<h3>2.1.2. isinstance()</h3>
<p>我们可以通过<code>isinstance()</code>来检查某个数据是否是我们指定的数据类型。</p>
<pre><code>print(isinstance(1, int))
print(isinstance(1, float))
</code></pre>
<p>输出：</p>
<pre><code>True
False
</code></pre>
<p>意思是1属于整形类，1不属于浮点型。</p>
<h2>2.2. 字符串</h2>
<h3>2.2.1. 字符串的定义</h3>
<p>字符串，顾名思义，就是由一系列字符组成的数据类型。在编程中，我们常用它来表示名字、短语、句子等文本内容。</p>
<p>在 Python 中，字符串不仅能存储单个字母，还可以包含数字、符号，甚至是空格。字符串是编程中最常用、最基础的数据类型之一。</p>
<p>在Python中，字符串可以用单引号（<code>'</code>）、双引号（<code>"</code>）、或三引号（<code>'''</code> 或 <code>"""</code>）括起来。字符串中的每一个字符都有其对应的索引。</p>
<p>例如：</p>
<pre><code>s1 = 'hello'
s2 = "world"
s3 = """多行字符串
可以换行
"""
print(s1)
print(s2)
print(s3)
</code></pre>
<p>输出：</p>
<pre><code>hello
world
多行字符串
可以换行
</code></pre>
<h4>字符串中的特殊字符</h4>
<p>字符串中有一些特殊字符，常用的有<code>\n</code>(换行)、<code>\t</code>(制表符)、<code>\\</code>(反斜杠)、<code>\"</code>(双引号)、<code>\'</code>(单引号)等。</p>
<p>例如：</p>
<pre><code>s = "Hello\nPython"
print(s)
s2 = "Hello\tPython"
print(s2)
s3 = "他说：\"你好，Python!\""
print(s3)
</code></pre>
<p>输出：</p>
<pre><code>Hello
Python
Hello	Python
他说："你好，Python!"
</code></pre>
<h4>转义与原始字符串</h4>
<p>有时我们不希望字符串中的<code>\</code>被当作特殊字符处理，可以在字符串前面加上<code>r</code>，变为原始字符串（raw string）。</p>
<pre><code>path = "C:\\Users\\Python"
print(path)
raw_path = r"C:\Users\Python"
print(raw_path)
</code></pre>
<p>输出：</p>
<pre><code>C:\Users\Python
C:\Users\Python
</code></pre>
<p>原始字符串常用于正则表达式、Windows路径等场景。</p>
<h3>2.2.2. 字符串常用操作</h3>
<h4>拼接字符串</h4>
<p>可以使用<code>+</code>号拼接两个字符串，也可以用<code>*</code>重复字符串。</p>
<pre><code>a = "Hello"
b = "Python"
c = a + " " + b
d = a * 3
print(c)  # 输出: Hello Python
print(d)  # 输出: HelloHelloHello
</code></pre>
<h4>获取字符串长度</h4>
<p>使用<code>len()</code>函数可以获取字符串的长度。</p>
<pre><code>msg = "Python"
print(len(msg))  # 输出: 6
</code></pre>
<h4>字符串索引与切片</h4>
<p>通过索引获取字符串中的某个字符，索引从0开始。通过切片可以获取一部分字符串。</p>
<pre><code>s = "abcdef"
print(s[0])    # 输出: a
print(s[-1])   # 输出: f
print(s[2:5])  # 输出: cde
print(s[:3])   # 输出: abc
</code></pre>
<h4>字符串常用方法</h4>
<ul>
<li><code>upper()</code>：转换成大写</li>
<li><code>lower()</code>：转换成小写</li>
<li><code>strip()</code>：去除首尾空白</li>
<li><code>replace(a, b)</code>：替换字符串中的a为b</li>
<li><code>split(sep)</code>：按照分隔符分割为列表</li>
</ul>
<pre><code>text = "  Python,Hello  "
print(text.upper())        # 输出:   PYTHON,HELLO  
print(text.lower())        # 输出:   python,hello  
print(text.strip())        # 输出: Python,Hello
print(text.replace("Hello", "World"))  # 输出:   Python,World  
print(text.split(","))     # 输出: ['  Python', 'Hello  ']
</code></pre>
<h4>字符串格式化</h4>
<p>可以用<code>f''</code>格式化字符串或者用<code>format()</code>方法。</p>
<pre><code>name = "Tom"
age = 18
print(f"My name is {name}, age is {age}")         # 推荐
print("My name is {}, age is {}".format(name, age))
</code></pre>
<p>输出：</p>
<pre><code>My name is Tom, age is 18
My name is Tom, age is 18
</code></pre>
<h2>2.3. 案例实操</h2>
<h3>2.3.1. 案例1</h3>
<p>根据自己的实际情况，输出个人的详细信息，具体结构如下：</p>
<p>"大家好，我是Hygen，今年18岁，学习的专业是软件工程，爱好Python、C++"</p>
<pre><code>name = "Hygen"
age = 18
pro = "软件工程"
hobby = "Python, C++"

print(f"大家好，我是{name}, 今年{age}岁，学习的专业是{pro}，爱好{hobby}")
</code></pre>
<h1>3. 输入与输出</h1>
<h2>3.1. input()</h2>
<p>input函数可以获取键盘输入的数据，用法为<code>s = input(提示信息)</code>，它的返回值是一个<strong>字符串</strong>
print函数可以将数据输出到控制台，用法为<code>print(需要输出的数据)</code></p>
<h2>3.2. 案例实战</h2>
<h3>3.2.1. 案例1</h3>
<p>小智的银行卡中有10000元，现在到ATM进行取钱操作，请根据输入的金额执行取钱操作，取钱完毕后，展示其银行卡余额。</p>
<pre><code># ATM取钱案例代码
money = 10000
draw = int(input("请输入要取款的金额："))
money -= draw
print(f"取钱成功，您的银行卡余额为：{money}")
</code></pre>
<p>示例输出</p>
<pre><code>请输入要取款的金额：2000
取钱成功，您的银行卡余额为：8000
</code></pre>
<h3>3.2.2. 案例2</h3>
<p>根据用户输入的两个数字，计算两个数之和，并将其输出到控制台。</p>
<pre><code># 获取输入
a, b = [int(x) for x in input().split()]
# 输出和
print(a + b)
</code></pre>
<p>示例输入</p>
<pre><code>3 4
</code></pre>
<p>示例输出</p>
<pre><code>7
</code></pre>
<h1>4. 运算符</h1>
<h1>4. 运算符</h1>
<h2>4.1. 算术运算符</h2>
<p>用于进行数学运算的符号，比如加减乘除等。</p>
<table>
<thead>
<tr>
<th>运算符</th>
<th>含义</th>
<th>示例</th>
<th>结果</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>+</code></td>
<td>加法</td>
<td><code>5 + 2</code></td>
<td>7</td>
</tr>
<tr>
<td><code>-</code></td>
<td>减法</td>
<td><code>5 - 2</code></td>
<td>3</td>
</tr>
<tr>
<td><code>*</code></td>
<td>乘法</td>
<td><code>5 * 2</code></td>
<td>10</td>
</tr>
<tr>
<td><code>/</code></td>
<td>除法（得到浮点数）</td>
<td><code>5 / 2</code></td>
<td>2.5</td>
</tr>
<tr>
<td><code>//</code></td>
<td>整除</td>
<td><code>5 // 2</code></td>
<td>2</td>
</tr>
<tr>
<td><code>%</code></td>
<td>取余（取模）</td>
<td><code>5 % 2</code></td>
<td>1</td>
</tr>
<tr>
<td><code>**</code></td>
<td>幂运算</td>
<td><code>5 ** 2</code></td>
<td>25</td>
</tr>
</tbody>
</table>
<h3>示例</h3>
<pre><code>a = 9
b = 4
print(a + b)  # 13
print(a - b)  # 5
print(a * b)  # 36
print(a / b)  # 2.25
print(a // b) # 2
print(a % b)  # 1
print(a ** b) # 6561
</code></pre>
<h2>4.2. 赋值运算符</h2>
<p>用于给变量赋值，也可以与算术运算结合（复合赋值运算符）。</p>
<table>
<thead>
<tr>
<th>运算符</th>
<th>含义</th>
<th>示例</th>
<th>结果</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>=</code></td>
<td>赋值</td>
<td><code>a = 5</code></td>
<td>a为5</td>
</tr>
<tr>
<td><code>+=</code></td>
<td>加法赋值</td>
<td><code>a += 2</code></td>
<td>等价于<code>a = a + 2</code></td>
</tr>
<tr>
<td><code>-=</code></td>
<td>减法赋值</td>
<td><code>a -= 1</code></td>
<td>等价于<code>a = a - 1</code></td>
</tr>
<tr>
<td><code>*=</code></td>
<td>乘法赋值</td>
<td><code>a *= 3</code></td>
<td>等价于<code>a = a * 3</code></td>
</tr>
<tr>
<td><code>/=</code></td>
<td>除法赋值</td>
<td><code>a /= 2</code></td>
<td>等价于<code>a = a / 2</code></td>
</tr>
<tr>
<td><code>//=</code></td>
<td>整除赋值</td>
<td><code>a //= 2</code></td>
<td>等价于<code>a = a // 2</code></td>
</tr>
<tr>
<td><code>%=</code></td>
<td>取余赋值</td>
<td><code>a %= 2</code></td>
<td>等价于<code>a = a % 2</code></td>
</tr>
<tr>
<td><code>**=</code></td>
<td>幂赋值</td>
<td><code>a **= 2</code></td>
<td>等价于<code>a = a ** 2</code></td>
</tr>
</tbody>
</table>
<h3>示例</h3>
<pre><code>a = 10
a += 5   # 变成15
a //= 3  # 15整除3，变成5
a **= 2  # 5的平方，变成25
print(a) # 25
</code></pre>
<h2>4.3. 比较运算符</h2>
<p>用于比较两个值，结果是布尔类型<code>True</code>或<code>False</code>。</p>
<table>
<thead>
<tr>
<th>运算符</th>
<th>含义</th>
<th>示例</th>
<th>结果</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>==</code></td>
<td>等于</td>
<td><code>3 == 5</code></td>
<td>False</td>
</tr>
<tr>
<td><code>!=</code></td>
<td>不等于</td>
<td><code>3 != 5</code></td>
<td>True</td>
</tr>
<tr>
<td><code>&gt;</code></td>
<td>大于</td>
<td><code>3 &gt; 5</code></td>
<td>False</td>
</tr>
<tr>
<td><code>&lt;</code></td>
<td>小于</td>
<td><code>3 &lt; 5</code></td>
<td>True</td>
</tr>
<tr>
<td><code>&gt;=</code></td>
<td>大于等于</td>
<td><code>3 &gt;= 3</code></td>
<td>True</td>
</tr>
<tr>
<td><code>&lt;=</code></td>
<td>小于等于</td>
<td><code>3 &lt;= 2</code></td>
<td>False</td>
</tr>
</tbody>
</table>
<h3>示例</h3>
<pre><code>a = 8
b = 6
print(a == b)  # False
print(a != b)  # True
print(a &gt; b)   # True
print(a &lt; b)   # False
print(a &gt;= 8)  # True
print(a &lt;= 5)  # False
</code></pre>
<h2>4.4. 逻辑运算符</h2>
<p>用于多个条件的组合判断。</p>
<table>
<thead>
<tr>
<th>运算符</th>
<th>含义</th>
<th>示例</th>
<th>结果</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>and</code></td>
<td>而且（并且）</td>
<td><code>True and False</code></td>
<td>False</td>
</tr>
<tr>
<td><code>or</code></td>
<td>或（两个中有一个为真就为真）</td>
<td><code>True or False</code></td>
<td>True</td>
</tr>
<tr>
<td><code>not</code></td>
<td>取反</td>
<td><code>not True</code></td>
<td>False</td>
</tr>
</tbody>
</table>
<h3>示例</h3>
<pre><code>age = 20
score = 85

print(age &gt; 18 and score &gt;= 80) # True
print(age &lt; 18 or score &gt;= 80)  # True
print(not (age == 20))          # False
</code></pre>
<p>常用于条件语句、循环控制等。</p>
]]></content>
    <author><name>Hygen</name></author>
  </entry>
  <entry>
    <title>论持久战</title>
    <link href="https://www.hygen.red/posts/learning/001%E8%AE%BA%E6%8C%81%E4%B9%85%E6%88%98/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/learning/001%E8%AE%BA%E6%8C%81%E4%B9%85%E6%88%98/</id>
    <published>2026-04-22T00:00:00.000Z</published>
    <updated>2026-04-22T00:00:00.000Z</updated>
    <summary>学习笔记</summary>
    <content type="html"><![CDATA[<p>这是一篇学习笔记</p>
]]></content>
    <author><name>Hygen</name></author>
  </entry>
  <entry>
    <title>FastAPI入门实战</title>
    <link href="https://www.hygen.red/posts/guides/002-fastapi/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/guides/002-fastapi/</id>
    <published>2025-12-17T00:00:00.000Z</published>
    <updated>2025-12-17T00:00:00.000Z</updated>
    <summary>简单入门及实战</summary>
    <content type="html"><![CDATA[<h1>1. FastAPI入门</h1>
<h2>1.1. 什么是FastAPI</h2>
<p><code>FastAPI</code> 是高性能的 <code>Python Web</code> 框架，专门用于构建 <code>API（应用程序接口）</code>。它基于标准的 Python <code>类型提示（type hints）</code>，是 Python 领域最受欢迎的 Web 框架之一。</p>
<h2>1.2. FastAPI有什么优点</h2>
<ol>
<li><code>高性能</code>：可大量处理并发请求</li>
<li><code>高效率</code>：类型检查，减少人为错误，开发速度可提升200%~300%</li>
<li><code>交互式</code>：自动生成Swagger UI文档(/docs)和ReDoc文档</li>
</ol>
<h1>2. 入门实战</h1>
<h2>2.1. 准备工作</h2>
<p><strong>准备虚拟环境</strong></p>
<p>此处使用<code>uv</code>来进行包管理。</p>
<ol>
<li>使用<code>uv init fastapi-demo</code>来初始化一个fastapi项目，进入项目文件夹</li>
<li>添加<code>fastapi</code>库：<code>uv add fastapi uvicorn[standard]</code></li>
</ol>
<h2>2.2. 第一个FastAPI项目</h2>
<p>在<code>main.py</code>中输入如下代码：</p>
<pre><code>from fastapi import FastAPI

# 注册FastAPI
app = FastAPI()

@app.get("/")
def read_root():
    return {"message": "Hello, FastAPI!"}


@app.get("/items/{item_id}")
def read_item(item_id: int, q: str = "query"):
    return {"item_id": item_id, "q": q}
</code></pre>
<p>在命令行中输入<code>uv run uvicorn main:app --reload</code>来运行第一个项目，在浏览器中输入<code>http://127.0.0.1:8000/</code>来访问这个项目，出现如下内容说明已经成功运行：</p>
<p><img src="./ac7514e3-222d-4362-87a9-cd0077cfb261.png" alt="first running" /></p>
<p>要访问api文档只需要在链接后面添加<code>/docs</code>就好了，完整链接：<code>http://127.0.0.1:8000/docs</code>，内容如下：</p>
<p><img src="./ada70338-ec49-4d1e-92ee-2f453f1b202d.png" alt="docs" /></p>
<h2>2.3. 路由</h2>
<p>路由简单来说就是<strong>URL地址</strong>和<strong>处理函数</strong>之间的映射关系，它决定了用户访问某个网址时，服务器应该执行哪段代码返回结果。</p>
<h3>路由实战</h3>
<blockquote>
<p>需求：当访问路径<code>/user/hello</code>时，服务器的响应结果是<code>{"msg":"Hello, Zako"}</code></p>
</blockquote>
<h4>代码</h4>
<p>main.py</p>
<pre><code>@app.get("/user/hello")
def hello():
    return {"message": "Hello, Zako!"}
</code></pre>
<p>运行结果和上面类似，不再贴出</p>
<h2>2.4. 参数</h2>
<p>参数就是客户端发送请求时<strong>额外附带的信息和指令</strong></p>
<p>参数的作用是让同一个接口能根据不同的输入，返回不同的输出，实现<strong>动态交互</strong></p>
<p>参数分为三类，<code>路径参数</code>, <code>查询参数</code>, <code>请求体</code></p>
<h3>2.4.1. 路径参数</h3>
<p><img src="./e2dd32ab-bc36-4821-9f43-45f19de8a447.png" alt="路径参数" /></p>
<h4>路径参数实战01</h4>
<blockquote>
<p>需求：以用户id为路径参数设计URL，要求响应结果包含用户id和名称（yusu+id）</p>
</blockquote>
<p>样例：</p>
<pre><code>URL： /user/114514
响应结果：{id: 114514, name: yusu114514}
</code></pre>
<h5>代码</h5>
<p>main.py</p>
<pre><code>@app.get("/user/{id}")
def get_user(id: int):
    return {"id": id, "name": f"yusu{id}"}
</code></pre>
<h4>路径参数实战02</h4>
<blockquote>
<p>需求： 定义两个接口，携带路径参数，并使用 Path 来实现类型注解</p>
<p>具体如下：</p>
<p>接口1： 以新闻分类id为参数设计URL，id范围为1~100</p>
<p>接口2： 以新闻分类名称为参数设计URL，分类名称长度为2~10</p>
</blockquote>
<h5>代码</h5>
<p>main.py</p>
<pre><code>@app.get("/news/{id}")
def get_news(id: int = Path(..., ge=1, le=100)):
    return {"id": id, "title": f"news{id}"}

@app.get("/news/{name}")
def get_name(name: str = Path(..., min_length=2, max_length=10)):
    return {"name": name}
</code></pre>
<h3>2.4.2. 查询参数</h3>
<p><img src="./02f293fd-b097-4596-b6fb-14eee1048746.png" alt="查询参数" /></p>
<h4>查询参数实战</h4>
<blockquote>
<p>需求：设计接口查询图书，要求携带两个查询参数：图书分类和价格</p>
<p>参数具体要求：</p>
<p>图书分类：默认值为 Python开发，长度限制5 ~ 255</p>
<p>价格：限制大小范围 50 ~ 100</p>
</blockquote>
<h5>代码</h5>
<p>main.py</p>
<pre><code>@app.get("/books")
def get_books(category: str = Query(default="Python开发", min_length=5, max_length=255), price: float = Query(ge=50, le=100)):
    return {"category": category, "price": price}
</code></pre>
<h3>2.4.3. 请求体</h3>
<p><img src="./5c636822-470f-4b83-9267-64e73478cdd5.png" alt="请求体" /></p>
<h4>请求体实战01</h4>
<blockquote>
<p>需求：设计接口新增图书，图书信息包含：书名、作者、出版社、售价</p>
</blockquote>
<h5>代码</h5>
<p>main.py</p>
<pre><code>class Book(BaseModel):
    title: str
    author: str
    publisher: str
    price: float

@app.post("/books")
def create_book(book: Book):
    return {"book": book}
</code></pre>
<h4>请求体实战02</h4>
<blockquote>
<p>需求：设计接口新增图书，图书信息包含：书名、作者、出版社、售价</p>
<p>具体要求如下：</p>
<p>书名：不能为空；长度 2 ~ 20</p>
<p>作者：长度 2 ~ 10</p>
<p>出版社：默认值“黑马出版社”</p>
<p>售价：不能为空；价格大于0元</p>
</blockquote>
<h5>代码</h5>
<p>main.py</p>
<pre><code>class Books(BaseModel):
    title: str = Field(..., min_length=2, max_length=20)
    author: str = Field(..., min_length=2, max_length=10)
    publisher: str = Field(default="黑马出版社")
    price: float = Field(..., gt=0)
@app.post("/book")
def create_book(book: Books):
    return {"book": book}
</code></pre>
<h2>2.4. 响应</h2>
<p>既然用户对服务器有请求，那么服务器自然需要有响应。</p>
<h3>2.4.1. 响应类型</h3>
<p>FastAPI 默认会将你路径操作函数返回的任何数据自动转换为 JSON，并使用<code>JSONResponse</code>作为默认响应。同时FastAPI也提供了丰富的响应类型和自定义的返回类型来返回不同的数据。</p>
<table>
<thead>
<tr>
<th>响应类型</th>
<th>用途</th>
<th>示例</th>
</tr>
</thead>
<tbody>
<tr>
<td>JSONResponse</td>
<td>默认响应，返回JSON数据</td>
<td>return {"key": "value"}</td>
</tr>
<tr>
<td>HTMLResponse</td>
<td>返回HTML内容</td>
<td>return HTMLResponse(html_content)</td>
</tr>
<tr>
<td>PlainTextResponse</td>
<td>返回纯文本</td>
<td>return PlainTextResponse("text")</td>
</tr>
<tr>
<td>FileResponse</td>
<td>返回文件下载</td>
<td>return FileResponse(path)</td>
</tr>
<tr>
<td>StreamingResponse</td>
<td>流式响应</td>
<td>生成器函数返回数据</td>
</tr>
<tr>
<td>RedirectResponse</td>
<td>重定向</td>
<td>return RedirectResponse(url)</td>
</tr>
<tr>
<td>其中最常用的是<code>JSON</code>、<code>HTML</code>、<code>File</code>格式。可以通过声明<code>response_class</code>参数或直接返回一个<code>Response</code>对象来覆盖默认行为，定制自己想要的响应类型。</td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
<h3>2.4.2. 响应类型设置方式</h3>
<p><img src="./982384ac-64e6-4137-ba8f-c1b9cb51aef2.png" alt="响应类型设置方式" /></p>
<h3>2.4.3. 自定义响应类型</h3>
<p><code>response_model</code>是路径操作装饰器（如 @app.get或 @app.post）的关键参数，它通过一个<code>Pydantic</code>模型来严格定义和约束 API 端
点的输出格式。这一机制在提供自动数据验证和序列化的同时，更是保障数据安全性的第一道防线。</p>
<pre><code>from pydantic import BaseModel
class News(BaseModel):
    id: int
    title: str
    content: str

@app.get("/news/{id}", response_model=News)
async def get_news(id: int):
    return {
        "id": id,
        "title": f"这是第{id}本书",
        "content": "这是一本好书"
}
</code></pre>
<h3>2.4.4. 异常处理</h3>
<p>对于客户端引发的错误（4xx，如资源未找到、认证失败），应使用<code>fastapi.HTTPException</code>来中断正常处理流程，
并返回标准错误响应 。</p>
<pre><code>from fastapi import FastAPI, HTTPException
@app.get('/news/{id}')
async def get_news(id: int):
    id_list = [1, 2, 3, 4, 5, 6]
    if id not in id_list:
        raise HTTPException(status_code=404, detail="当前id不存在")
    return {"id": id}
</code></pre>
<h1>3. FastAPI进阶</h1>
<h2>3.1. 中间件</h2>
<p>中间件（Middleware）是一个在每次请求进入 FastAPI 应用时都会被执行的函数。它在请求到达实际的路径操作（路由处理函数）之前运行，并且在响应返回给客户端之前再运行一次。</p>
<h3>3.1.1. 中间件的作用</h3>
<p>中间件可以为每个请求添加统一的处理逻辑，常见应用场景包括：</p>
<ul>
<li>记录日志</li>
<li>身份认证</li>
<li>跨域处理</li>
<li>设置响应头</li>
<li>性能监控</li>
</ul>
<h3>3.1.2. 中间件的定义</h3>
<p>在函数的顶部使用装饰器 <code>@app.middleware("http")</code> 来定义中间件。</p>
<pre><code>@app.middleware("http")
async def middleware(request, call_next):
    print('中间件开始处理 -- start')
    response = await call_next(request)
    print('中间件处理完成 -- end')
    return response
</code></pre>
<h3>3.1.3. 多个中间件的执行顺序</h3>
<p>多个中间件的执行顺序是<strong>自下而上</strong>，即后定义的中间件先执行。</p>
<h2>3.2. 依赖注入</h2>
<p>依赖注入（Dependency Injection）是 FastAPI 的核心特性之一，用于共享通用逻辑，减少代码重复。</p>
<h3>3.2.1. 什么是依赖注入</h3>
<ul>
<li><strong>依赖项</strong>：可重用的组件（函数/类），负责提供某种功能或数据</li>
<li><strong>注入</strong>：FastAPI 自动帮你调用依赖项，并将结果"注入"到路径操作函数中</li>
</ul>
<h3>3.2.2. 依赖注入的优点</h3>
<ul>
<li><strong>代码复用</strong>：一次编写，多处使用</li>
<li><strong>解耦</strong>：业务逻辑与基础设施代码分离</li>
<li><strong>易于测试</strong>：轻松地用模拟依赖替换真实依赖进行测试</li>
</ul>
<h3>3.2.3. 依赖注入的应用场景</h3>
<ol>
<li><strong>处理请求参数</strong>：从请求中提取和验证参数（路径参数、查询参数、请求体）</li>
<li><strong>共享数据库连接</strong>：管理数据库会话的创建、使用、关闭</li>
<li><strong>共享业务逻辑</strong>：抽取封装多个路由公用的逻辑代码</li>
<li><strong>安全和认证</strong>：验证用户身份、检查权限和角色要求等</li>
</ol>
<h3>3.2.4. 依赖注入的使用</h3>
<p>使用依赖注入的步骤：</p>
<ol>
<li>创建依赖项（函数）</li>
<li>导入 <code>Depends</code></li>
<li>在路径操作函数中声明依赖项</li>
</ol>
<h4>依赖注入实战</h4>
<blockquote>
<p>需求：多个接口都需要分页逻辑，使用依赖注入来共享分页参数处理</p>
</blockquote>
<h5>代码</h5>
<p>main.py</p>
<pre><code>from fastapi import FastAPI, Depends, Query

app = FastAPI()

async def common_parameters(
    skip: int = Query(0, ge=0),
    limit: int = Query(10, le=60)
):
    return {"skip": skip, "limit": limit}

@app.get("/news/news_list")
async def get_news_list(commons = Depends(common_parameters)):
    return {"list": "新闻列表", **commons}

@app.get("/users/user_list")
async def get_user_list(commons = Depends(common_parameters)):
    return {"users": "用户列表", **commons}
</code></pre>
<h2>3.3. ORM</h2>
<p>ORM（Object-Relational Mapping，对象关系映射）是一种编程技术，用于在面向对象的编程语言和关系型数据库之间建立映射关系。它允许开发者使用面向对象的方式操作数据库，而不需要直接编写 SQL 语句。</p>
<h3>3.3.1. SQLAlchemy ORM</h3>
<p>SQLAlchemy 是 Python 中最流行的 ORM 框架，也是企业应用最多、社区最活跃的 ORM 库。</p>
<h3>3.3.2. 安装和配置</h3>
<h4>安装依赖</h4>
<pre><code>uv add sqlalchemy[asyncio] aiomysql
</code></pre>
<h4>创建数据库引擎</h4>
<pre><code>from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker

# 创建异步引擎
engine = create_async_engine(
    "mysql+aiomysql://user:password@localhost/dbname",
    echo=True
)

# 创建会话工厂
async_session = sessionmaker(
    engine, class_=AsyncSession, expire_on_commit=False
)

# 依赖项：获取数据库会话
async def get_database():
    async with async_session() as session:
        yield session
</code></pre>
<h3>3.3.3. 定义模型</h3>
<h4>定义模型基类</h4>
<pre><code>from sqlalchemy.orm import DeclarativeBase

class Base(DeclarativeBase):
    pass
</code></pre>
<h4>定义模型类</h4>
<pre><code>from sqlalchemy import Column, Integer, String, Float
from sqlalchemy.orm import DeclarativeBase

class Base(DeclarativeBase):
    pass

class Book(Base):
    __tablename__ = "books"
    
    id = Column(Integer, primary_key=True, index=True)
    bookname = Column(String(100), nullable=False)
    author = Column(String(50))
    price = Column(Float)
</code></pre>
<h4>创建数据表</h4>
<pre><code>from sqlalchemy import run_sync

# 创建所有表
async def init_db():
    async with engine.begin() as conn:
        await run_sync(Base.metadata.create_all, conn)
</code></pre>
<h3>3.3.4. 数据库操作</h3>
<h4>查询数据</h4>
<h5>查询所有数据</h5>
<pre><code>from sqlalchemy import select

@app.get("/book/get_books")
async def get_book_list(db: AsyncSession = Depends(get_database)):
    stmt = select(Book)
    result = await db.execute(stmt)
    books = result.scalars().all()
    return {"books": books}
</code></pre>
<h5>查询单条数据</h5>
<pre><code># 方式1：使用 get 方法（根据主键）
book = await db.get(Book, book_id)

# 方式2：使用 select + first
stmt = select(Book).where(Book.id == book_id)
result = await db.execute(stmt)
book = result.scalars().first()

# 方式3：使用 scalar_one_or_none（确保只有一条或没有）
book = result.scalars().one_or_none()
</code></pre>
<h5>分页查询</h5>
<pre><code>@app.get("/book/get_books")
async def get_book_list(
    page: int = 1,
    page_size: int = 3,
    db: AsyncSession = Depends(get_database)
):
    skip = (page - 1) * page_size
    stmt = select(Book).offset(skip).limit(page_size)
    result = await db.execute(stmt)
    books = result.scalars().all()
    return {"books": books}
</code></pre>
<h4>新增数据</h4>
<p>核心步骤：定义 ORM 对象 → 添加对象到事务：<code>add(对象)</code> → <code>commit</code> 提交到数据库</p>
<pre><code>from pydantic import BaseModel

class BookBase(BaseModel):
    bookname: str
    author: str
    price: float

@app.post("/book/add_book")
async def add_book(book: BookBase, db: AsyncSession = Depends(get_database)):
    # 创建图书对象（**book.dict() 返回 book 对象的属性字典）
    book_obj = Book(**book.dict())
    db.add(book_obj)
    await db.commit()
    return book
</code></pre>
<h4>更新数据</h4>
<p>核心步骤：查询 <code>get</code> → 属性重新赋值 → <code>commit</code> 提交到数据库</p>
<pre><code>from pydantic import BaseModel
from fastapi import HTTPException

class BookUpdate(BaseModel):
    bookname: str
    author: str
    price: float

@app.put("/book/update_book/{book_id}")
async def update_book(
    book_id: int, 
    data: BookUpdate, 
    db: AsyncSession = Depends(get_database)
):
    # 1. 查询
    book = await db.get(Book, book_id)
    if book is None:
        raise HTTPException(status_code=404, detail="Book not found")
    # 2. 修改属性(重新赋值)
    book.bookname = data.bookname
    book.author = data.author
    book.price = data.price
    # 3. 提交
    await db.commit()
    return book
</code></pre>
<h4>删除数据</h4>
<p>核心步骤：查询 <code>get</code> → <code>delete</code> 删除 → <code>commit</code> 提交到数据库</p>
<pre><code>@app.delete("/book/delete_book/{book_id}")
async def delete_book(
    book_id: int, 
    db: AsyncSession = Depends(get_database)
):
    db_book = await db.get(Book, book_id)
    if db_book is None:
        raise HTTPException(status_code=404, detail="Book not found")
    await db.delete(db_book)
    await db.commit()
    return {"message": "Book deleted"}
</code></pre>
<h3>3.3.5. ORM 查询总结</h3>
<h4>核心思路</h4>
<ul>
<li><code>select()</code> → <code>db.execute()</code> → 从 ORM 对象获取数据 → 响应结果</li>
<li><code>db.get(模型类, 主键值)</code></li>
</ul>
<h4>从 ORM 对象获取数据的方式</h4>
<ul>
<li><strong>获取所有数据</strong>：<code>scalars().all()</code></li>
<li><strong>获取单条数据</strong>：
<ul>
<li><code>scalars().first()</code>：提取第一个数据</li>
<li><code>scalar_one_or_none()</code>：提取一个或 null</li>
<li><code>scalar()</code>：提取标量值（配合聚合查询使用）</li>
</ul>
</li>
</ul>
<h3>3.3.6. SQLAlchemy ORM 总结</h3>
<h4>安装</h4>
<ul>
<li><code>sqlalchemy[asyncio]</code></li>
<li><code>aiomysql</code>（异步数据库驱动）</li>
</ul>
<h4>建表</h4>
<ol>
<li><code>create_async_engine</code> 创建异步引擎</li>
<li>定义模型基类（继承 <code>DeclarativeBase</code>）和模型类</li>
<li><code>run_sync(Base.metadata.create_all)</code> 建表</li>
</ol>
<h4>操作数据</h4>
<ol>
<li>路由处理函数注入数据库会话依赖项 <code>Depends</code></li>
<li>查询数据：<code>select()</code></li>
<li>增加数据：<code>add()</code></li>
<li>更新数据：重新赋值</li>
<li>删除数据：<code>delete()</code></li>
</ol>
]]></content>
    <author><name>Hygen</name></author>
  </entry>
  <entry>
    <title>如何参与开源项目</title>
    <link href="https://www.hygen.red/posts/guides/001%E5%8F%82%E4%B8%8E%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/guides/001%E5%8F%82%E4%B8%8E%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE/</id>
    <published>2025-11-29T00:00:00.000Z</published>
    <updated>2025-11-29T00:00:00.000Z</updated>
    <summary>作为初学者首次参与到开源项目中</summary>
    <content type="html"><![CDATA[<p>在开始之前，先确保自己的电脑里有<code>git</code>，如果没有的话需要先去<a href="https://git-scm.com/">git官网</a>下载，并配置，还要先了解基础的<code>clone</code>,<code>add</code>,<code>commit</code>,<code>push</code>,<code>pull</code>等基本操作</p>
<h1>开源项目选择</h1>
<p>github中有一个开源项目是专门用于让初学者学习如何参与开源项目的，链接如下：</p>
<p><a href="https://github.com/firstcontributions/first-contributions">first-contributions</a></p>
<p>这个开源项目的README文档中有具体的参与教程，这里直接根据这个开源项目的教程来学习如何参与开源项目</p>
<h1>参与开源项目</h1>
<h2>1. Fork代码仓库</h2>
<p>在进入上面的网页中后，点击右上角的<code>Fork</code>，后面的确认信息直接保持默认即可</p>
<p><img src="./ff9b007b-f52c-4776-8b67-39677edaa677.png" alt="fork" /></p>
<h2>2. Clone代码仓库</h2>
<p>在得到自己的仓库后，选择将自己的库克隆到本地，注意是<strong>自己的仓库</strong>而不是原仓库</p>
<p><img src="./a246f705-2897-4dbe-a643-577c733cd11e.png" alt="clone" /></p>
<p>在本地找个文件夹右键打开<code>git bash</code>，然后在命令行中输入以下内容</p>
<p><img src="./153e51ba-8343-486f-aff0-95344d10b77b.png" alt="git bash" /></p>
<pre><code>git clone [复制的内容]
</code></pre>
<h2>3. 新建代码分支</h2>
<p>随后在<code>git bash</code>中进入仓库，并创建新的代码分支，分支名可以是<code>add-newbranch</code>，也可以是<code>add-hygen</code>，可以是任意名字，但是最好有<code>add-</code>表示添加分支</p>
<pre><code>cd first-contributions
git switch -c add-newbranch
</code></pre>
<h2>4. 修改，并提交修改</h2>
<p>由于这是开源教程，所以没有实质性的代码修改内容，打开<code>Contributions.md</code>这个文件，在最后加上自己的名字即可</p>
<p><img src="./1dcc73ac-7f40-4315-9347-7d4731332719.png" alt="update" /></p>
<p>将修改提交上去前可以用<code>git status</code>先看看都有哪些修改</p>
<p><img src="./8e1df43e-4781-4fa2-a37f-75ae34500c3f.png" alt="status" /></p>
<p>之后提交修改，如果要提交所有修改，就选择<code>git add .</code>，这里选择精确提交修改，<code>commit</code>处将<code>yourname</code>部分改成自己的名字即可</p>
<pre><code>git add Contributions.md
git commit -m "Add &lt;yourname&gt; to Contributions list"
</code></pre>
<p><img src="./993f10b2-5f5f-4b39-a94c-5ed2fe63c3f3.png" alt="commit" /></p>
<h2>5. 将改动push到github</h2>
<p>使用<code>git push</code>推送代码，后面同样将内容替换为自己修改的新分支名称即可</p>
<pre><code>git push origin &lt;new-branch-name&gt;
</code></pre>
<p>example：</p>
<pre><code>git push origin add-hygen
</code></pre>
<h2>6， 提出pull request将修改供他人审阅</h2>
<p>前往自己的仓库，有<code>Compare &amp; pull request</code>按钮，点击即可</p>
<p><img src="./3740071b-aef3-460f-80be-004e476bf542.png" alt="pull request" /></p>
<h1>注意事项</h1>
<p>关于正式开始后有哪些注意事项，请参考b站up主码农高天的<a href="https://www.bilibili.com/video/BV15C411r7uD/?spm_id_from=333.1391.0.0&amp;vd_source=277b2219bdd7d053a64be57e1ece8b6d">这期视频</a></p>
]]></content>
    <author><name>Hygen</name></author>
  </entry>
  <entry>
    <title>手撕kmeans（简易版）</title>
    <link href="https://www.hygen.red/posts/dsa/solution/003kmeans%E6%9C%BA%E5%99%A8%E5%88%86%E7%B1%BB/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/dsa/solution/003kmeans%E6%9C%BA%E5%99%A8%E5%88%86%E7%B1%BB/</id>
    <published>2025-11-20T00:00:00.000Z</published>
    <updated>2025-11-20T00:00:00.000Z</updated>
    <summary>华为11.19机试题第21题</summary>
    <content type="html"><![CDATA[<h1>题目描述</h1>
<p>简单来说就是给定一个输入<code>k, m, n</code></p>
<ul>
<li><code>k</code>表示聚类的数量</li>
<li><code>m</code>表示样本的数量</li>
<li><code>n</code>表示训练轮数</li>
</ul>
<p>样本的特征维度固定是<code>4</code></p>
<p>接下来<code>m</code>行输入每个样本的值，每行包含4个<code>浮点数</code>，每个浮点数表示一个特征</p>
<p>要求直接选择前<code>k</code>个样本作为初始化质心</p>
<p>训练结束条件为<code>训练次数达到n轮</code>或<code>新簇和旧簇的距离&lt;1e-8</code>，后者意为如果训练前后两个簇位置没变，则说明已经稳定</p>
<p>输入保证所有数据都合法且已归一化</p>
<p>输出：</p>
<p>要输出<code>k</code>个值，表示这<code>k</code>个聚类中每个簇有几个样本，按<code>从小到大</code>的顺序排列</p>
<h1>样例</h1>
<h2>输入</h2>
<pre><code>3 20 1000
0.11 0.79 0.68 0.97
1.0 0.8 0.13 0.33
0.27 0.02 0.5 0.46
0.83 0.29 0.23 0.75
0.97 0.08 0.84 0.55
0.29 0.71 0.17 0.83
0.03 0.6 0.88 0.28
0.24 0.26 0.82 0.03
0.96 0.12 0.82 0.36
0.13 0.12 0.86 0.44
0.23 0.7 0.35 0.06
0.42 0.49 0.67 0.84
0.8 0.49 0.47 0.7
0.68 0.03 0.11 0.07
0.77 0.19 0.95 0.44
0.25 0.12 0.98 0.04
0.7 0.11 0.53 0.3
0.73 0.67 0.46 0.96
0.11 0.31 0.91 0.57
0.43 0.61 0.13 0.1
</code></pre>
<p>表示将20个样本分为3个簇，训练1000轮</p>
<h2>输出</h2>
<pre><code>4 6 10
</code></pre>
<p>表示分为的三个簇中第一个簇有4个样本，第二个簇有6个样本，第三个簇有10个样本</p>
<h1>解题思路</h1>
<h2>核心思想</h2>
<p>K-means 是一种经典的<strong>无监督聚类算法</strong>，其目标是将 $m$ 个样本划分为 $k$ 个簇，使得同一簇内的样本相似度高，不同簇之间的样本相似度低。</p>
<p>算法的核心思想是：</p>
<ol>
<li>初始化 $k$ 个聚类中心</li>
<li>将每个样本分配到最近的聚类中心</li>
<li>根据分配结果更新聚类中心（取簇内样本的均值）</li>
<li>重复步骤 2-3，直到聚类中心收敛或达到最大迭代次数</li>
</ol>
<h2>算法步骤</h2>
<h3>1. 初始化聚类中心</h3>
<p>根据题意，直接选择前 $k$ 个样本作为初始聚类中心。</p>
<pre><code>  def _init_centroids(self, data):
    self.centroids = data[: self.n_clusters]
</code></pre>
<h3>2. 计算距离矩阵</h3>
<p>对于每个样本，计算其到所有 $k$ 个聚类中心的<strong>欧氏距离</strong>。使用 NumPy 的广播机制高效计算，并将结果存储在 <code>self.distances</code> 中：</p>
<pre><code>  def _distance(self, data):
    self.distances = np.sqrt(((data[:, np.newaxis] - self.centroids) ** 2).sum(axis=2))
</code></pre>
<blockquote>
<p>📖 <strong>补充知识</strong>：关于 <code>np.newaxis</code>、<code>np.sqrt</code>、<code>np.sum</code> 和 <code>axis=2</code> 的详细说明，请参考 <a href="#%E8%B7%9D%E7%A6%BB%E8%AE%A1%E7%AE%97%E7%9B%B8%E5%85%B3%E5%87%BD%E6%95%B0">距离计算相关函数</a></p>
</blockquote>
<h3>3. 分配样本到最近的簇</h3>
<p>使用 <code>argmin</code> 找到每个样本距离最近的聚类中心索引。该方法直接使用 <code>self.distances</code> 中存储的距离矩阵：</p>
<pre><code>  def _min_distance(self):
    return np.argmin(self.distances, axis=1)
</code></pre>
<blockquote>
<p>📖 <strong>补充知识</strong>：关于 <code>np.argmin</code> 和 <code>axis=1</code> 的详细说明，请参考 <a href="#argmin-%E5%87%BD%E6%95%B0%E8%AF%A6%E8%A7%A3">argmin 函数详解</a></p>
</blockquote>
<h3>4. 更新聚类中心</h3>
<p>对于每个簇，计算簇内所有样本的<strong>均值</strong>作为新的聚类中心。使用列表推导式高效计算，并返回新旧聚类中心的欧氏范数（用于判断收敛）：</p>
<pre><code>  def _update_centroids(self, data):
    old = self.centroids
    self.centroids = np.array([data[self.labels == i].mean(axis=0) for i in range(self.n_clusters)])
    return np.linalg.norm(old - self.centroids)
</code></pre>
<blockquote>
<p>📖 <strong>补充知识</strong>：关于 <code>np.mean</code>、<code>axis=0</code> 和 <code>np.linalg.norm</code> 的详细说明，请参考 <a href="#%E6%9B%B4%E6%96%B0%E8%81%9A%E7%B1%BB%E4%B8%AD%E5%BF%83%E7%9B%B8%E5%85%B3%E5%87%BD%E6%95%B0">更新聚类中心相关函数</a> 和 <a href="#%E8%8C%83%E6%95%B0%E8%AE%A1%E7%AE%97%E5%87%BD%E6%95%B0">范数计算函数</a></p>
</blockquote>
<h3>5. 迭代训练</h3>
<p>重复执行分配和更新步骤，直到满足以下条件之一：</p>
<ul>
<li>达到最大迭代次数 $n$</li>
<li>聚类中心的变化小于阈值 $10^{-8}$（使用欧氏范数衡量）</li>
</ul>
<pre><code>  def fit(self, data):
    self._init_centroids(data)
    for _ in range(self.max_iter):
      self._distance(data)
      self.labels = self._min_distance()
      if self._update_centroids(data) &lt; self.tolerance:
        break
</code></pre>
<blockquote>
<p>📖 <strong>补充知识</strong>：关于 <code>np.linalg.norm</code> 的详细说明，请参考 <a href="#%E8%8C%83%E6%95%B0%E8%AE%A1%E7%AE%97%E5%87%BD%E6%95%B0">范数计算函数</a></p>
</blockquote>
<h3>6. 统计并输出结果</h3>
<p>训练完成后，统计每个簇的样本数量，并按从小到大排序输出：</p>
<pre><code>  cnt = np.bincount(kmeans.labels, minlength=k)
  cnt.sort()
  print(*cnt)
</code></pre>
<blockquote>
<p>📖 <strong>补充知识</strong>：关于 <code>np.bincount</code>、<code>np.array</code> 和 <code>reshape</code> 的详细说明，请参考 <a href="#%E6%95%B0%E6%8D%AE%E7%BB%9F%E8%AE%A1%E7%9B%B8%E5%85%B3%E5%87%BD%E6%95%B0">数据统计相关函数</a></p>
</blockquote>
<h2>关键实现细节</h2>
<ol>
<li>
<p><strong>距离计算优化</strong>：使用 NumPy 的广播机制一次性计算所有样本到所有聚类中心的距离，避免嵌套循环，提高效率。距离矩阵存储在 <code>self.distances</code> 中，供后续使用。</p>
</li>
<li>
<p><strong>代码结构优化</strong>：</p>
<ul>
<li>使用私有方法（以下划线开头）封装内部实现细节</li>
<li><code>_update_centroids</code> 方法使用列表推导式简化代码，并直接返回收敛度</li>
<li><code>_min_distance</code> 方法直接使用 <code>self.distances</code>，无需传递参数</li>
</ul>
</li>
<li>
<p><strong>收敛判断</strong>：<code>_update_centroids</code> 方法直接返回新旧聚类中心的欧氏范数，在 <code>fit</code> 方法中判断是否小于阈值 $10^{-8}$ 来决定是否提前终止迭代。</p>
</li>
</ol>
<h2>时间复杂度</h2>
<ul>
<li><strong>时间复杂度</strong>：$O(m \cdot k \cdot d \cdot t)$，其中 $m$ 是样本数，$k$ 是聚类数，$d$ 是特征维度（本题为 4），$t$ 是迭代次数。每次迭代需要计算距离矩阵和更新聚类中心。</li>
<li><strong>空间复杂度</strong>：$O(m \cdot k + k \cdot d)$，用于存储距离矩阵、标签和聚类中心。</li>
</ul>
<h2>注意事项</h2>
<ul>
<li>K-means 算法对初始聚类中心的选择敏感，不同的初始化可能导致不同的聚类结果。</li>
<li>算法保证收敛，但可能收敛到局部最优解而非全局最优解。</li>
<li>本题中特征维度固定为 4，简化了实现复杂度。</li>
</ul>
<h1>代码实现</h1>
<pre><code>import numpy as np
import sys

class Kmeans:
  def __init__(self, n_clusters, max_iter):
    self.n_clusters = n_clusters
    self.max_iter = max_iter
    self.centroids = None
    self.labels = None
    self.distances = None
    self.tolerance = 1e-8

  def _init_centroids(self, data):
    self.centroids = data[: self.n_clusters]

  def _distance(self, data):
    self.distances = np.sqrt(((data[:, np.newaxis] - self.centroids) ** 2).sum(axis=2))

  def _min_distance(self):
    return np.argmin(self.distances, axis=1)

  def _update_centroids(self, data):
    old = self.centroids
    self.centroids = np.array([data[self.labels == i].mean(axis=0) for i in range(self.n_clusters)])
    return np.linalg.norm(old - self.centroids)

  def fit(self, data):
    self._init_centroids(data)
    for _ in range(self.max_iter):
      self._distance(data)
      self.labels = self._min_distance()
      if self._update_centroids(data) &lt; self.tolerance:
        break


def main():
  data = sys.stdin.read().split()
  idx = 0
  k, m, n = [int(x) for x in data[idx:idx+3]]
  idx += 3
  X = np.array(data[idx:idx+4*m], dtype=np.float64).reshape(m, 4)
  kmeans = Kmeans(n_clusters=k, max_iter=n)
  kmeans.fit(X)
  cnt = np.bincount(kmeans.labels, minlength=k)
  cnt.sort()
  print(*cnt)

if __name__ == '__main__':
  main()
</code></pre>
<h1>补充知识</h1>
<h2>距离计算相关函数</h2>
<h3><code>np.newaxis</code></h3>
<p><strong>定义</strong>：<code>np.newaxis</code> 是 <code>None</code> 的别名，用于数组索引时增加维度。</p>
<p><strong>示例</strong>：</p>
<pre><code>arr = np.array([1, 2, 3])  # shape: (3,)
arr_new = arr[:, np.newaxis]  # shape: (3, 1)
# 结果: [[1], [2], [3]]
</code></pre>
<h3><code>np.sqrt</code></h3>
<p><strong>定义</strong>：对数组中的每个元素计算平方根。</p>
<p><strong>示例</strong>：</p>
<pre><code>arr = np.array([4, 9, 16])
result = np.sqrt(arr)  # [2.0, 3.0, 4.0]
</code></pre>
<h3><code>np.sum</code></h3>
<p><strong>定义</strong>：<code>np.sum(a, axis=None)</code> 计算数组元素的和，<code>axis</code> 指定沿哪个轴求和。</p>
<p><strong>示例</strong>：</p>
<pre><code>arr = np.array([[1, 2], [3, 4]])  # shape: (2, 2)
np.sum(arr, axis=0)  # 沿第0轴（行）求和: [4, 6]
np.sum(arr, axis=1)  # 沿第1轴（列）求和: [3, 7]
np.sum(arr)  # 所有元素求和: 10
</code></pre>
<h3>距离计算的详细过程</h3>
<ol>
<li>
<p><strong>形状变换</strong>：</p>
<ul>
<li><code>X</code> 的形状为 $(m, 4)$，表示 $m$ 个样本，每个样本有 4 个特征</li>
<li><code>X[:, np.newaxis]</code> 将形状变为 $(m, 1, 4)$，增加了一个维度</li>
<li><code>self.centroids</code> 的形状为 $(k, 4)$，表示 $k$ 个聚类中心</li>
</ul>
</li>
<li>
<p><strong>广播运算</strong>：</p>
<ul>
<li><code>X[:, np.newaxis] - self.centroids</code> 通过广播机制，将 $(m, 1, 4)$ 和 $(k, 4)$ 进行运算</li>
<li>广播规则：将 <code>self.centroids</code> 扩展为 $(1, k, 4)$，然后与 $(m, 1, 4)$ 运算</li>
<li>结果形状为 $(m, k, 4)$，表示每个样本到每个聚类中心在每个特征维度上的差值</li>
</ul>
</li>
<li>
<p><strong>平方、求和和开方</strong>：</p>
<ul>
<li><code>** 2</code> 对差值进行平方，形状仍为 $(m, k, 4)$</li>
<li><code>.sum(axis=2)</code> <strong>沿第 2 轴（特征维度）求和</strong>，将 4 个特征维度的平方差相加</li>
<li>结果形状变为 $(m, k)$，表示每个样本到每个聚类中心的欧氏距离的平方</li>
<li><code>np.sqrt(...)</code> 对整个求和结果开方，得到最终的欧氏距离矩阵</li>
<li>注意：括号 <code>((...).sum(axis=2))</code> 确保了先对平方差求和，然后再对结果开方，这是正确的欧氏距离计算公式</li>
</ul>
</li>
</ol>
<p><strong>为什么使用 <code>axis=2</code>？</strong></p>
<ul>
<li>数组形状为 $(m, k, 4)$，三个维度分别表示：样本索引、聚类中心索引、特征维度</li>
<li><code>axis=2</code> 表示沿特征维度（第 3 个维度，索引为 2）求和</li>
<li>这样可以将每个样本到每个聚类中心在 4 个特征上的平方差相加，得到总的平方距离</li>
<li>如果使用 <code>axis=0</code> 或 <code>axis=1</code>，会沿错误的方向求和，无法得到正确的距离</li>
</ul>
<p><strong>运算顺序说明</strong>：</p>
<p>代码 <code>np.sqrt(((data[:, np.newaxis] - self.centroids) ** 2).sum(axis=2))</code> 的执行顺序是：</p>
<ol>
<li>计算差值并平方：<code>(data[:, np.newaxis] - self.centroids) ** 2</code> → 形状 $(m, k, 4)$</li>
<li>沿特征维度求和：<code>.sum(axis=2)</code> → 形状 $(m, k)$，得到平方距离</li>
<li>对平方距离开方：<code>np.sqrt(...)</code> → 形状 $(m, k)$，得到欧氏距离</li>
</ol>
<p>外层括号 <code>((...).sum(axis=2))</code> 确保了求和操作在开方之前完成，这符合欧氏距离的数学定义：$\sqrt{\sum_{i=1}^{d}(x_i - y_i)^2}$，即先对各个维度的平方差求和，再对总和开方。</p>
<h2>argmin 函数详解</h2>
<h3><code>np.argmin</code></h3>
<p><strong>定义</strong>：<code>np.argmin(a, axis=None)</code> 返回沿指定轴的最小值的索引。</p>
<p><strong>示例</strong>：</p>
<pre><code>arr = np.array([[3, 1, 4], [2, 5, 1]])  # shape: (2, 3)
np.argmin(arr, axis=0)  # 沿第0轴找最小值索引: [1, 0, 1]
np.argmin(arr, axis=1)  # 沿第1轴找最小值索引: [1, 2]
np.argmin(arr)  # 全局最小值索引（扁平化后）: 1
</code></pre>
<h3>为什么使用 <code>axis=1</code>？</h3>
<ul>
<li><code>dts</code> 的形状为 $(m, k)$，表示 $m$ 个样本到 $k$ 个聚类中心的距离</li>
<li><code>axis=1</code> 表示沿第 1 轴（聚类中心维度）查找最小值</li>
<li>对于每个样本（第 0 轴），我们在 $k$ 个聚类中心（第 1 轴）中找到距离最小的那个</li>
<li>结果是一个长度为 $m$ 的一维数组，每个元素表示对应样本最近的聚类中心索引</li>
</ul>
<p><strong>示例说明</strong>：</p>
<pre><code># 假设 dts = [[2.5, 1.2, 3.1],  # 样本0到3个聚类中心的距离
#            [1.8, 2.3, 0.9],  # 样本1到3个聚类中心的距离
#            [3.2, 2.1, 1.5]]  # 样本2到3个聚类中心的距离
# np.argmin(dts, axis=1) 返回: [1, 2, 2]
# 表示：样本0最近的是聚类中心1，样本1最近的是聚类中心2，样本2最近的是聚类中心2
</code></pre>
<h2>更新聚类中心相关函数</h2>
<h3><code>np.mean</code></h3>
<p><strong>定义</strong>：<code>np.mean(a, axis=None)</code> 计算数组元素的平均值，<code>axis</code> 指定沿哪个轴计算。</p>
<p><strong>示例</strong>：</p>
<pre><code>arr = np.array([[1, 2, 3], [4, 5, 6]])  # shape: (2, 3)
np.mean(arr, axis=0)  # 沿第0轴（行）求均值: [2.5, 3.5, 4.5]
np.mean(arr, axis=1)  # 沿第1轴（列）求均值: [2.0, 5.0]
np.mean(arr)  # 所有元素的均值: 3.5
</code></pre>
<h3>更新过程的详细说明</h3>
<p>优化后的代码使用列表推导式简化了实现：</p>
<ol>
<li>
<p><strong>提取每个簇的样本并计算均值</strong>：</p>
<ul>
<li><code>data[self.labels == i]</code> 使用布尔索引提取属于第 $i$ 个簇的所有样本</li>
<li><code>.mean(axis=0)</code> <strong>沿第 0 轴（样本维度）求均值</strong></li>
<li>对于每个簇，计算簇内所有样本在每个特征维度上的平均值</li>
<li>结果形状为 $(4,)$，表示 4 个特征的平均值</li>
</ul>
</li>
<li>
<p><strong>构建新的聚类中心数组</strong>：</p>
<ul>
<li><code>np.array([...])</code> 将列表推导式的结果转换为数组</li>
<li>最终 <code>self.centroids</code> 的形状为 $(k, 4)$，每行代表一个聚类中心</li>
</ul>
</li>
<li>
<p><strong>计算收敛度</strong>：</p>
<ul>
<li><code>old = self.centroids</code> 保存更新前的聚类中心</li>
<li><code>np.linalg.norm(old - self.centroids)</code> 计算新旧聚类中心的欧氏范数</li>
<li>返回范数值，用于在 <code>fit</code> 方法中判断是否收敛</li>
</ul>
</li>
</ol>
<p><strong>为什么使用 <code>axis=0</code>？</strong></p>
<ul>
<li><code>samples</code> 的形状为 $(n_i, 4)$，包含 $n_i$ 个样本，每个样本有 4 个特征</li>
<li><code>axis=0</code> 表示沿样本维度（第 0 轴）进行聚合操作</li>
<li>对于每个特征维度（第 1 轴），我们对所有样本在该特征上的值求平均</li>
<li>这样可以得到一个 4 维的向量，表示该簇在所有特征上的平均位置</li>
<li>如果使用 <code>axis=1</code>，会沿特征维度求均值，得到每个样本的平均特征值，这是错误的</li>
</ul>
<p><strong>示例说明</strong>：</p>
<pre><code># 假设第0个簇有3个样本
samples = np.array([[1.0, 2.0, 3.0, 4.0],  # 样本1
                    [1.5, 2.5, 3.5, 4.5],  # 样本2
                    [2.0, 3.0, 4.0, 5.0]]) # 样本3
# shape: (3, 4)

# np.mean(samples, axis=0) 对每列（特征）求均值
# 结果: [1.5, 2.5, 3.5, 4.5]  # 4个特征的平均值

# 如果使用 axis=1，会对每行（样本）求均值
# np.mean(samples, axis=1) 结果: [2.5, 3.0, 3.5]  # 每个样本的平均特征值（错误）
</code></pre>
<h2>范数计算函数</h2>
<h3><code>np.linalg.norm</code></h3>
<p><strong>定义</strong>：<code>np.linalg.norm(x, ord=None, axis=None)</code> 计算数组的范数，默认计算欧氏范数（L2 范数）。</p>
<p><strong>示例</strong>：</p>
<pre><code>vec = np.array([3, 4])
np.linalg.norm(vec)  # 欧氏范数: 5.0 (sqrt(3^2 + 4^2))

matrix = np.array([[1, 2], [3, 4]])
np.linalg.norm(matrix)  # 矩阵的Frobenius范数: 5.477...
np.linalg.norm(matrix, axis=0)  # 沿第0轴计算: [3.162..., 4.472...]
</code></pre>
<p><strong>收敛判断说明</strong>：</p>
<ul>
<li><code>self.centroids - old</code> 计算新旧聚类中心的差值，形状为 $(k, 4)$</li>
<li><code>np.linalg.norm()</code> 计算所有差值的欧氏范数，得到一个标量值</li>
<li>如果这个值小于阈值 $10^{-8}$，说明所有聚类中心的变化都很小，算法已收敛</li>
</ul>
<h2>数据统计相关函数</h2>
<h3><code>np.bincount</code></h3>
<p><strong>定义</strong>：<code>np.bincount(x, weights=None, minlength=0)</code> 返回一个数组，索引 <code>i</code> 处的值表示 <code>i</code> 在输入数组中出现的次数。</p>
<p><strong>示例</strong>：</p>
<pre><code>labels = np.array([0, 1, 1, 0, 2, 1])
cnt = np.bincount(labels)  # [2, 3, 1] 表示0出现2次，1出现3次，2出现1次
cnt = np.bincount(labels, minlength=5)  # [2, 3, 1, 0, 0] 保证输出长度至少为5
</code></pre>
<p><strong>统计过程说明</strong>：</p>
<ul>
<li><code>kmeans.labels</code> 是一个长度为 $m$ 的一维数组，每个元素表示对应样本所属的簇索引（0 到 $k-1$）</li>
<li><code>np.bincount(kmeans.labels, minlength=k)</code> 统计每个簇索引出现的次数</li>
<li><code>minlength=k</code> 确保输出数组长度至少为 $k$，即使某些簇没有样本也会输出 0</li>
<li>结果 <code>cnt</code> 是一个长度为 $k$ 的数组，<code>cnt[i]</code> 表示第 $i$ 个簇的样本数量</li>
<li><code>cnt.sort()</code> 对数组进行原地排序，从小到大排列</li>
<li><code>print(*cnt)</code> 使用解包操作打印数组元素，用空格分隔</li>
</ul>
<h3><code>np.array</code></h3>
<p><strong>定义</strong>：从列表或其他数组创建 NumPy 数组。</p>
<p><strong>示例</strong>：</p>
<pre><code>data = ['1.0', '2.0', '3.0', '4.0']
arr = np.array(data, dtype=np.float64)  # [1.0, 2.0, 3.0, 4.0]
</code></pre>
<h3><code>reshape</code></h3>
<p><strong>定义</strong>：改变数组的形状而不改变数据。</p>
<p><strong>示例</strong>：</p>
<pre><code>arr = np.array([1, 2, 3, 4, 5, 6])
arr.reshape(2, 3)  # [[1, 2, 3], [4, 5, 6]]
arr.reshape(3, 2)  # [[1, 2], [3, 4], [5, 6]]
</code></pre>
<p><strong>数据读取说明</strong>：</p>
<ul>
<li><code>np.array(data[idx:idx+4*m], dtype=np.float64)</code> 将字符串列表转换为 NumPy 数组</li>
<li>切片 <code>data[idx:idx+4*m]</code> 提取从索引 <code>idx</code> 开始的 $4m$ 个元素（Python 切片是左闭右开区间）</li>
<li>因为有 $m$ 个样本，每个样本有 4 个特征，所以总共需要 $4m$ 个浮点数</li>
<li><code>.reshape(m, 4)</code> 将一维数组重塑为 $(m, 4)$ 的二维数组，每行代表一个样本</li>
</ul>
]]></content>
    <author><name>Hygen</name></author>
  </entry>
  <entry>
    <title>LangGraph中的工具调用代理</title>
    <link href="https://www.hygen.red/posts/agent/langgraph/%E5%8E%9F%E7%90%86/005-tool_calling/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/agent/langgraph/%E5%8E%9F%E7%90%86/005-tool_calling/</id>
    <published>2025-11-19T00:00:00.000Z</published>
    <updated>2025-11-19T00:00:00.000Z</updated>
    <summary>LangGraph框架中Tool Calling Agent（工具调用代理）的核心概念、ToolNode使用方法、完整实现案例及手动构建方法详解</summary>
    <content type="html"><![CDATA[<h1>工具调用代理概述</h1>
<p><code>Tool Calling Agent</code>（工具调用代理）是<code>LangGraph</code>支持的第二种<code>AI Agent</code>代理架构。<strong>这个代理架构是在<code>Router Agent</code>的基础上，大模型可以自主选择并使用多种工具来完成某个条件分支中的任务。</strong></p>
<p>工具调用大家应该非常熟悉了，当我们希望代理与外部系统交互时，工具就非常有用。外部系统（例如API）通常需要特定的输入模式，而不是自然语言。例如，当我们绑定 API 作为工具时，我们赋予大模型对所需输入模式的感知，大模型就能根据用户的自然语言输入选择调用工具，并将返回符合该工具架构的输出。</p>
<p>&lt;div align=center&gt;&lt;img src="https://muyu001.oss-cn-beijing.aliyuncs.com/img/image-20241024112245580.png" width=100%&gt;&lt;/div&gt;</p>
<p><code>Tool Calling Agent</code>的本质原理是：让大模型根据用户的输入，自动的去判断应该使用哪个函数，并实际的执行，最后结合工具的响应结果 + 用户的原始问题作为完整的<code>Prompt</code>生成最终的问题。即如下图所示：</p>
<p>&lt;div align=center&gt;&lt;img src="https://muyu001.oss-cn-beijing.aliyuncs.com/img/image-20241025120544561.png" width=100%&gt;&lt;/div&gt;</p>
<h1>ToolNode的使用</h1>
<h2>ToolNode的核心概念</h2>
<p>在<code>LangGraph</code>框架中，可以直接使用预构建<code>ToolNode</code>进行工具调用，其内部实现原理和我们之前介绍的手动实现的<code>Function Calling</code>流程思路基本一致，即：</p>
<pre><code>tools_by_name = {tool.name: tool for tool in tools}
def tool_node(state: dict):
    result = []
    for tool_call in state["messages"][-1].tool_calls:
        tool = tools_by_name[tool_call["name"]]
        observation = tool.invoke(tool_call["args"])
        result.append(ToolMessage(content=observation, tool_call_id=tool_call["id"]))
    return {"messages": result}
</code></pre>
<p>经过<code>ToolNode</code>工具后，其返回的是一个<code>LangChain Runnable</code>对象，会<strong>将图形状态（带有消息列表）作为输入并输出状态更新以及工具调用的结果</strong>，通过这种设计去适配<code>LangGraph</code>中其他的功能组件。</p>
<p>对于<code>ToolNode</code>的使用，有三个必要的点需要满足：</p>
<ol>
<li><strong>状态必须包含消息列表。</strong></li>
<li><strong>最后一条消息必须是AIMessage。</strong></li>
<li><strong>AIMessage必须填充tool_calls。</strong></li>
</ol>
<h2>工具的定义与装饰</h2>
<p>先尝试一下联网功能</p>
<pre><code>import requests
import json

def fetch_real_time_info(query):
  """Get real-time Internet information"""
    
  url = "https://google.serper.dev/search"
    
  payload = json.dumps({
    "q": query,
    "num": 1,
  })
    
  headers = {
    'X-API-KEY': 'xxx', # 填写自己的serper-key, 可以进入 url 的网址中注册账号获取
    'Content-Type': 'application/json'
  }
    
  response = requests.post(url, headers=headers, data=payload, timeout=10)
  
  data = json.loads(response.text)
  
  if 'organic' in data:
      return json.dumps(data['organic'],  ensure_ascii=False)  # 返回'organic'部分的JSON字符串
  else:
      return json.dumps({"error": "No organic results found"},  ensure_ascii=False)  # 如果没有'organic'键，返回错误信息

print(fetch_real_time_info("DeepSeek的最新新闻"))
</code></pre>
<p>实验结果：</p>
<pre><code>[{"title": "更新日志", "link": "https://api-docs.deepseek.com/zh-cn/updates", "snippet": "新闻. DeepSeek-V3.2-Exp 发布2025/09/29 · DeepSeek V3.1 更新2025/09/22 · DeepSeek V3.1 发布2025/08/21 · DeepSeek-R1-0528 发布2025/05/28 · DeepSeek-V3-0324 发布 ...", "position": 1}]
</code></pre>
<p>如果想要将普通的函数变成<code>ToolNode</code>可以应用的外部函数，只需要在函数定义时添加<code>@tool</code>装饰器。</p>
<pre><code>from langchain_core.tools import tool
import requests
import json

@tool
def fetch_real_time_info(query):
  """Get real-time Internet information"""
    
  url = "https://google.serper.dev/search"
    
  payload = json.dumps({
    "q": query,
    "num": 1,
  })
    
  headers = {
    'X-API-KEY': 'xxx', # 使用自己的api-key
    'Content-Type': 'application/json'
  }
    
  response = requests.post(url, headers=headers, data=payload, timeout=10)
  
  data = json.loads(response.text)
  
  if 'organic' in data:
      return json.dumps(data['organic'],  ensure_ascii=False)  # 返回'organic'部分的JSON字符串
  else:
      return json.dumps({"error": "No organic results found"},  ensure_ascii=False)  # 如果没有'organic'键，返回错误信息

print(f"""name: {fetch_real_time_info.name}
description: {fetch_real_time_info.description}
arguments: {fetch_real_time_info.args}""")
</code></pre>
<p>实验结果</p>
<pre><code>name: fetch_real_time_info
description: Get real-time Internet information
arguments: {'query': {'title': 'Query'}}
</code></pre>
<p>使用<code>@tool</code>装饰器后，函数会自动获得以下属性：</p>
<ul>
<li><code>name</code>: 工具名称</li>
<li><code>description</code>: 工具描述（从函数文档字符串中提取）</li>
<li><code>args</code>: 工具参数定义</li>
</ul>
<h2>ToolNode的实例化</h2>
<p>使用<code>ToolNode</code>对函数进行实例化：</p>
<pre><code>from langgraph.prebuilt import ToolNode

tools = [fetch_real_time_info]
tool_node = ToolNode(tools)
print(tool_node)
</code></pre>
<p>实验结果</p>
<pre><code>tools(tags=None, recurse=True, explode_args=False, func_accepts_config=True, func_accepts={'store': ('__pregel_store', None)}, tools_by_name={'fetch_real_time_info': StructuredTool(name='fetch_real_time_info', description='Get real-time Internet information', args_schema=&lt;class 'langchain_core.utils.pydantic.fetch_real_time_info'&gt;, func=&lt;function fetch_real_time_info at 0x00000238629AD1C0&gt;)}, tool_to_state_args={'fetch_real_time_info': {}}, tool_to_store_arg={'fetch_real_time_info': None}, handle_tool_errors=True, messages_key='messages')
</code></pre>
<p><code>ToolNode</code>使用消息列表对图状态进行操作。所以它要求消息列表中的最后一条消息是带有<code>tool_calls</code>参数的<code>AIMessage</code>。</p>
<h3>单个工具调用示例</h3>
<pre><code>from langchain_core.messages import AIMessage

message_with_single_tool_call = AIMessage(
  content="",
  tool_calls=[
    {
      "name": "fetch_real_time_info",
      "args": {"query": "DeepSeek的最新新闻"},
      "id": "tool_call_id",
      "type": "tool_call",
    }
  ],
)
print(tool_node.invoke({"messages": [message_with_single_tool_call]}))
</code></pre>
<p>实验结果</p>
<pre><code>{'messages': [ToolMessage(content='[{"title": "DeepSeek | 世界新聞網", "link": "https://www.worldjournal.com/search/tagging/8877/DeepSeek", "snippet": "FT：DeepSeek新模型延後問世問題出在華為晶片還是得靠輝達 · 2025-08-14 03:00 ; OpenAI最新模型GPT-5發布傳DeepSeek將推R2 · 2025-08-14 02:04 ; 受惠DeepSeek-R2將發表中國AI ...", "position": 1}]', name='fetch_real_time_info', tool_call_id='tool_call_id')]}
</code></pre>
<h3>并行工具调用示例</h3>
<p>如果将多个工具调用同时传递给<code>AIMessage</code>的<code>tool_calls</code>参数，仍然可以使用<code>ToolNode</code>进行并行工具调用：</p>
<pre><code>@tool
def get_weather(location):
    """Call to get the current weather."""
    if location.lower() in ["beijing"]:
        return "北京的温度是16度，天气晴朗。"
    elif location.lower() in ["shanghai"]:
        return "上海的温度是20度，部分多云。"
    else:
        return "不好意思，并未查询到具体的天气信息。"

tools = [fetch_real_time_info, get_weather]
tool_node = ToolNode(tools)
print(tool_node)
</code></pre>
<p>实验结果</p>
<pre><code>tools(tags=None, recurse=True, explode_args=False, func_accepts_config=True, func_accepts={'store': ('__pregel_store', None)}, tools_by_name={'fetch_real_time_info': StructuredTool(name='fetch_real_time_info', description='Get real-time Internet information', args_schema=&lt;class 'langchain_core.utils.pydantic.fetch_real_time_info'&gt;, func=&lt;function fetch_real_time_info at 0x00000178B4F7D580&gt;), 'get_weather': StructuredTool(name='get_weather', description='Get the weather of a location', args_schema=&lt;class 'langchain_core.utils.pydantic.get_weather'&gt;, func=&lt;function get_weather at 0x00000178B4F71120&gt;)}, tool_to_state_args={'fetch_real_time_info': {}, 'get_weather': {}}, tool_to_store_arg={'fetch_real_time_info': None, 'get_weather': None}, handle_tool_errors=True, messages_key='messages')
</code></pre>
<pre><code>message_with_multiple_tool_calls = AIMessage(
  content="",
  tool_calls=[
    {
      "name": "fetch_real_time_info",
      "args": {"query": "DeepSeek的最新新闻"},
      "id": "tool_call_id",
      "type": "tool_call",
    },
    {
      "name": "get_weather",
      "args": {"location": "beijing"},
      "id": "tool_call_id_2",
      "type": "tool_call",
    },
  ],
)
print(tool_node.invoke({"messages": [message_with_multiple_tool_calls]}))
</code></pre>
<p>实验结果</p>
<pre><code>{'messages': [ToolMessage(content='[{"title": "DeepSeek-V3.2-Exp 发布，训练推理提效，API 同步降价", "link": "https://api-docs.deepseek.com/zh-cn/news/news250929", "snippet": "在新的价格政策下，开发者调用DeepSeek API 的成本将降低50% 以上。 目前API 的模型版本为DeepSeek-V3.2-Exp，访问方式保持
不变。 欢迎用户使用DeepSeek 官方的API 服务。", "position": 1}]', name='fetch_real_time_info', tool_call_id='tool_call_id'), ToolMessage(content='北京的温度是16度，天气晴朗。', name='get_weather', tool_call_id='tool_call_id_2')]}
</code></pre>
<h2>大模型与工具绑定</h2>
<p>通过<code>bind_tools</code>函数可以将工具绑定到大模型，让大模型知道有哪些工具可以使用：</p>
<pre><code>from langchain_openai import ChatOpenAI
import os

os.environ["OPENAI_API_KEY"] = "sk-xxx" # 自己的api-key
llm = ChatOpenAI(
  base_url="https://api.deepseek.com/v1",
  model="deepseek-chat",
  temperature=0
)

model_with_tools = llm.bind_tools(tools)
print(model_with_tools)
</code></pre>
<p>实验结果</p>
<pre><code>bound=ChatOpenAI(client=&lt;openai.resources.chat.completions.completions.Completions object at 0x000001D4A259DAF0&gt;, async_client=&lt;openai.resources.chat.completions.completions.AsyncCompletions object at 0x000001D4A2502E10&gt;, root_client=&lt;openai.OpenAI object at 0x000001D4A10CA570&gt;, root_async_client=&lt;openai.AsyncOpenAI object at 0x000001D4A259DF70&gt;, model_name='deepseek-chat', temperature=0.0, model_kwargs={}, openai_api_key=SecretStr('**********'), openai_api_base='https://api.deepseek.com/v1') kwargs={'tools': [{'type': 'function', 'function': {'name': 'fetch_real_time_info', 'description': 'Get real-time Internet information', 'parameters': {'properties': {'query': {}}, 'required': ['query'], 'type': 'object'}}}, {'type': 'function', 'function': {'name': 'get_weather', 'description': 'Get the weather of a location', 'parameters': {'properties': {'location': {}}, 'required': ['location'], 'type': 'object'}}}]} config={} config_factories=[]
</code></pre>
<p>绑定后，大模型可以根据用户的自然语言输入，自动生成<code>tool_calls</code>信息：</p>
<pre><code># 大模型会自动判断需要调用哪个工具
response = model_with_tools.invoke("DeepSeek的最新新闻")
print(response.tool_calls) # 输出工具调用信息

# 可以直接传递给ToolNode执行
print(tool_node.invoke({"messages": [response]}))
</code></pre>
<p>实验结果</p>
<pre><code>[{'name': 'fetch_real_time_info', 'args': {'query': 'DeepSeek最新新闻'}, 'id': 'call_00_LmIQTb8iSy0BHM7iuQPoZV9N', 'type': 'tool_call'}]
{'messages': [ToolMessage(content='[{"title": "DeepSeek解决AI有无问题，灵光攻克AI好用难题", "link": "https://t.cj.sina.cn/articles/view/6328613060/m17936f8c403301lueq?from=tech&amp;vt=4", "snippet": "如果说DeepSeek解决了AI“有没有”的问题，那么灵光则解决了AI“好不好用”的难题. 打开App看更多精彩内容. 分享文章到.", "date": "19 hours ago", "position": 1}]', name='fetch_real_time_info', tool_call_id='call_00_LmIQTb8iSy0BHM7iuQPoZV9N')]}
</code></pre>
<h1>工具调用代理的完整实现案例</h1>
<h2>场景设计</h2>
<p>对<code>Router Agent</code>实现的图做进一步的升级，即用户输入问题后，如果不需要外部工具的信息，则直接生成回复，否则，则进入一个工具库中，选择最合适的工具执行，并返回最终的响应。</p>
<h2>工具库定义</h2>
<p>首先定义多个工具，使用<code>Pydantic</code>模型来定义工具的参数结构：</p>
<pre><code>import json
from tokenize import String
from langchain_core.tools import tool
from pydantic import Field, BaseModel
import requests
from langgraph.prebuilt import ToolNode
from typing import Optional

from sqlalchemy import Column, Integer, String, create_engine, text
from sqlalchemy.orm import declarative_base, sessionmaker

# 定义搜索查询模型
class SearchQuery(BaseModel):
  query: str = Field(description="Questions for networking queries")

@tool(args_schema=SearchQuery)
def fetch_real_time_info(query):
  """Get real-time Internet information"""
  url = "https://google.serper.dev/search"
  payload = json.dumps({"q": query, "num": 1})
  headers = {
    'X-API-KEY': 'xxx', # 你的api-key
    'Content-Type': 'application/json'
  }
  response = requests.post(url, headers=headers, data=payload)
  data = json.loads(response.text)
  if 'organic' in data:
    return json.dumps(data['organic'], ensure_ascii=False)
  else:
    return json.dumps({"error": "No organic results found"}, ensure_ascii=False)

class WeatherLoc(BaseModel):
  location: str = Field(description="The location name of the city")

# 定义获取天气工具
@tool(args_schema=WeatherLoc)
def get_weather(location):
  """Get the weather of a location"""
  if location.lower() in ["beijing"]:
    return "北京的温度是16度，天气晴朗。"
  elif location.lower() in ["shanghai"]:
    return "上海的温度是20度，部分多云。"
  else:
    return "不好意思，并未查询到具体的天气信息。"

# 创建基类
Base = declarative_base()

# 定义 UserInfo 模型
class User(Base):
  __tablename__ = 'users'
  id = Column(Integer, primary_key=True)
  name = Column(String(50))
  age = Column(Integer)
  email = Column(String(100))
  phone = Column(String(15))

class UserInfo(BaseModel):
  """Extracted user information, such as name, age, email, and phone number, if relevant."""
  name: str = Field(description="The name of the user")
  age: Optional[int] = Field(description="The age of the user")
  email: str = Field(description="The email address of the user")
  phone: Optional[str] = Field(description="The phone number of the user")

# MySQL 连接信息（不包含数据库名，用于创建数据库）
MYSQL_USER = 'root'
MYSQL_PASSWORD = '123456'
MYSQL_HOST = 'localhost'
MYSQL_PORT = '3306'
DATABASE_NAME = 'langgraph_agent'

# 先连接到 MySQL 服务器（不指定数据库），用于创建数据库
server_uri = f'mysql+pymysql://{MYSQL_USER}:{MYSQL_PASSWORD}@{MYSQL_HOST}:{MYSQL_PORT}?charset=utf8mb4'
server_engine = create_engine(server_uri, echo=False)

# 创建数据库（如果不存在）
with server_engine.connect() as conn:
    conn.execute(text(f"CREATE DATABASE IF NOT EXISTS `{DATABASE_NAME}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"))
    conn.commit()

# 连接到指定的数据库
DATABASE_URI = f'mysql+pymysql://{MYSQL_USER}:{MYSQL_PASSWORD}@{MYSQL_HOST}:{MYSQL_PORT}/{DATABASE_NAME}?charset=utf8mb4'
engine = create_engine(DATABASE_URI, echo=True)

# 如果表不存在，则创建表
Base.metadata.create_all(engine)

# 创建会话
Session = sessionmaker(bind=engine)
session = Session()

# 定义插入数据库工具
@tool(args_schema=UserInfo)
def insert_db(name, age, email, phone):
  """Insert user information into the database, The required parameters are name, age, email, phone"""
  session = Session()
  try:
    user = User(name=name, age=age, email=email, phone=phone)
    session.add(user)
    session.commit()
    return "用户信息已成功插入数据库。"
  except Exception as e:
    session.rollback()
    return f"用户信息插入失败: {e}"
  finally:
    session.close()
</code></pre>
<p>在三个工具定义完毕后，将其传递给<code>tool_node</code>，并将其实例化</p>
<pre><code>tools = [fetch_real_time_info, get_weather, insert_db]
tool_node = ToolNode(tools)
print(tool_node)
</code></pre>
<p>实验结果</p>
<pre><code>tools(tags=None, recurse=True, explode_args=False, func_accepts_config=True, func_accepts={'store': ('__pregel_store', None)}, tools_by_name={'fetch_real_time_info': StructuredTool(name='fetch_real_time_info', description='Get real-time Internet information', args_schema=&lt;class '__main__.SearchQuery'&gt;, func=&lt;function fetch_real_time_info at 0x00000239B8965300&gt;), 'get_weather': StructuredTool(name='get_weather', description='Get the weather of a location', args_schema=&lt;class '__main__.WeatherLoc'&gt;, func=&lt;function get_weather at 0x00000239B89B9580&gt;), 'insert_db': StructuredTool(name='insert_db', description='Insert user information into the database, The required parameters are name, age, email, phone', args_schema=&lt;class '__main__.UserInfo'&gt;, func=&lt;function insert_db at 0x00000239B8BCDC60&gt;)}, tool_to_state_args={'fetch_real_time_info': {}, 'get_weather': {}, 'insert_db': {}}, tool_to_store_arg={'fetch_real_time_info': None, 'get_weather': None, 'insert_db': None}, handle_tool_errors=True, messages_key='messages')
</code></pre>
<h2>结构化输出模型定义</h2>
<pre><code>from langchain_openai import ChatOpenAI
import os

os.environ["OPENAI_API_KEY"] = "your-openai-api-key-here"  # 占位符：请换成你自己的密钥，切勿把真实密钥写进文章或仓库

llm = ChatOpenAI(
  base_url="https://api.deepseek.com/v1",
  model="deepseek-chat",
  temperature=0
)

model_with_tools = llm.bind_tools(tools)
</code></pre>
<p>使用<code>Pydantic</code>来做结构化输出，帮助<code>Router Function</code>来选择路径分支：</p>
<pre><code>from typing import Union

# 定义正常响应模型，可以是用户信息或一般响应
class ConversationalResponse(BaseModel):
  """Respond to the user's query in a conversational manner. Be kind and helpful."""
  response: str = Field(description="A conversational response to the user's query")

# 定义最终响应模型，可以是用户信息或一般响应
class FinalResponse(BaseModel):
  final_output: Union[ConversationalResponse, SearchQuery, WeatherLoc, UserInfo]
</code></pre>
<h2>节点函数定义</h2>
<p>定义三个节点函数：</p>
<pre><code>def chat_with_model(state):
  """generate structured output"""
  print(state)
  print("-----------------")
  messages = state['messages']
  structured_llm = llm.with_structured_output(FinalResponse, method="function_calling")
  response = structured_llm.invoke(messages)
  return {"messages": [response]}

def final_answer(state):
  """generate natural language responses"""
  print(state)
  print("-----------------")
  messages = state['messages'][-1]
  response = messages.final_output.response
  return {"messages": [response]}

def execute_function(state):
  """execute tool calling"""
  print(state)
  print("-----------------")
  final_output = state['messages'][-1].final_output
  
  # 将结构化输出转换为 AIMessage，包含工具调用
  from langchain_core.messages import AIMessage
  
  # 根据不同的输出类型构建工具调用
  tool_calls = []
  if isinstance(final_output, SearchQuery):
    tool_calls.append({
      "name": "fetch_real_time_info",
      "args": {"query": final_output.query},
      "id": "tool_call_search",
      "type": "tool_call",
    })
  elif isinstance(final_output, WeatherLoc):
    tool_calls.append({
      "name": "get_weather",
      "args": {"location": final_output.location},
      "id": "tool_call_weather",
      "type": "tool_call",
    })
  elif isinstance(final_output, UserInfo):
    tool_calls.append({
      "name": "insert_db",
      "args": {
        "name": final_output.name,
        "age": final_output.age,
        "email": final_output.email,
        "phone": final_output.phone
      },
      "id": "tool_call_db",
      "type": "tool_call",
    })
  
  # 创建包含工具调用的 AIMessage
  ai_message = AIMessage(content="", tool_calls=tool_calls)
  
  # 调用工具节点
  tool_response = tool_node.invoke({"messages": [ai_message]})
  
  # 提取工具执行结果
  if tool_response.get("messages") and len(tool_response["messages"]) &gt; 0:
    # ToolMessage 的内容就是工具的执行结果
    result = tool_response["messages"][0].content
  else:
    result = "工具执行失败：未返回结果"
  
  print("工具执行结果:", result)
  return {"messages": [result]}
</code></pre>
<h2>定义图的状态模式</h2>
<pre><code>from typing import TypedDict, Annotated
import operator
from langchain_core.messages import AnyMessage

class AgentState(TypedDict):
  messages: Annotated[list[AnyMessage], operator.add]
</code></pre>
<h2>路由函数定义</h2>
<p>定义<code>generate_branch</code>函数作为<code>Router Function</code>，根据经过<code>chat_with_model</code>节点后产生的不同<code>Pydantic</code>对象，选择连接不同的节点：</p>
<pre><code>def generate_branch(state: AgentState):
  result = state['messages'][-1]
  output = result.final_output
  if isinstance(output, ConversationalResponse):
    return False # 不需要工具调用
  else:
    return True # 需要工具调用
</code></pre>
<h2>图结构构建</h2>
<p>构建完整的图结构：</p>
<pre><code># 构建图结构
from langgraph.graph import StateGraph

graph = StateGraph(AgentState)

graph.add_node("chat_with_model", chat_with_model)
graph.add_node("final_answer", final_answer)
graph.add_node("execute_function", execute_function)

graph.set_entry_point("chat_with_model")

graph.add_conditional_edges(
  "chat_with_model",
  generate_branch,
  {True: "execute_function", False: "final_answer"}
)

graph.set_finish_point("final_answer")
graph.set_finish_point("execute_function")

graph = graph.compile()
</code></pre>
<h2>测试示例</h2>
<p>完成图的编译后，可以开始进行功能测试：</p>
<pre><code>from langchain_core.messages import HumanMessage

# 测试1: 普通对话（不需要工具）
query = "你好，请你介绍一下你自己"
input_message = {"messages": [HumanMessage(content=query)]}
result = graph.invoke(input_message)
print("-----------------")
print(result)
print("-----------------")
print(result["messages"][-1])

# 测试2: 网络搜索（需要工具）
query = "帮我查一下DeepSeek的最新新闻"
input_message = {"messages": [HumanMessage(content=query)]}
result = graph.invoke(input_message)
print("-----------------")
print(result)
print("-----------------")
print(result["messages"][-1])

# 测试3: 天气查询（需要工具）
query = "Beijing的天气怎么样？"
input_message = {"messages": [HumanMessage(content=query)]}
result = graph.invoke(input_message)
print("-----------------")
print(result)
print("-----------------")
print(result["messages"][-1])

# 测试4: 数据库插入（需要工具）
query = "我是Hygen，今年18，电话号是01000721，邮箱是ciallo@qq.com"
input_message = {"messages": [HumanMessage(content=query)]}
result = graph.invoke(input_message)
print("-----------------")
print(result)
print("-----------------")
print(result["messages"][-1])
</code></pre>
<p>实验结果</p>
<pre><code>{'messages': [HumanMessage(content='你好，请你介绍一下你自己', additional_kwargs={}, response_metadata={})]}
-----------------
{'messages': [HumanMessage(content='你好，请你介绍一下你自己', additional_kwargs={}, response_metadata={}), FinalResponse(final_output=ConversationalResponse(response='你好！我是一个AI助手，很高兴认识你！我可以帮助你回答各种问题、提供信息、协助解决问题，或者只是和你聊天。我具备多种能力，包
括搜索信息、处理文本、分析数据等。\n\n我的特点是：\n- 能够理解和回应中文\n- 可以处理各种类型的问题\n- 提供准确和有用的信息\n- 保持友好和专业的交流方式\n\n如果你有任何问题或需要帮助，请随时告诉我！你想了解什么具体内容，或者有什么我可以为你做的吗？'))]}
-----------------
-----------------
{'messages': [HumanMessage(content='你好，请你介绍一下你自己', additional_kwargs={}, response_metadata={}), FinalResponse(final_output=ConversationalResponse(response='你好！我是一个AI助手，很高兴认识你！我可以帮助你回答各种问题、提供信息、协助解决问题，或者只是和你聊天。我具备多种能力，包 括搜索信息、处理文本、分析数据等。\n\n我的特点是：\n- 能够理解和回应中文\n- 可以处理各种类型的问题\n- 提供准确和有用的信息\n- 保持友好和专业的交流方式\n\n如果你有任何问题或需要帮助，请随时告诉我！你想了解什么具体内容，或者有什么我可以为你做的吗？')), '你好！我是一个AI助手，很高兴认识你！我 可以帮助你回答各种问题、提供信息、协助解决问题，或者只是和你聊天。我具备多种能力，包括搜索信息、处理文本、分析数据等。\n\n我的特点是：\n- 能够理解和回应中文\n- 可以处理各种类型的问题\n- 提供准确和有用的信息\n- 保持友好和专业的交流方式\n\n如果你有任何问题或需要帮助，请随时告诉我！你想了解什 么具体内容，或者有什么我可以为你做的吗？']}
-----------------
你好！我是一个AI助手，很高兴认识你！我可以帮助你回答各种问题、提供信息、协助解决问题，或者只是和你聊天。我具备多种能力，包括搜索信息、处理文本、分析数据等。

我的特点是：
- 能够理解和回应中文
- 可以处理各种类型的问题
- 提供准确和有用的信息
- 保持友好和专业的交流方式

如果你有任何问题或需要帮助，请随时告诉我！你想了解什么具体内容，或者有什么我可以为你做的吗？
{'messages': [HumanMessage(content='帮我查一下DeepSeek的最新新闻', additional_kwargs={}, response_metadata={})]}
-----------------
{'messages': [HumanMessage(content='帮我查一下DeepSeek的最新新闻', additional_kwargs={}, response_metadata={}), FinalResponse(final_output=SearchQuery(query='DeepSeek最新新闻'))]}
-----------------
工具执行结果: [{"title": "DeepSeek等中製AI模型操弄資訊、洩個資國安局", "link": "https://www.cna.com.tw/news/aipl/202511160037.aspx", "snippet": "國安局今天表示，依法抽驗包括DeepSeek等5款中製「生成式AI語言模型」後，所生成內容出現中共官宣、歷史偏差、資訊操弄等嚴重偏頗與不實資訊， ...", "date": "3 days ago", "position": 1}]
-----------------
{'messages': [HumanMessage(content='帮我查一下DeepSeek的最新新闻', additional_kwargs={}, response_metadata={}), FinalResponse(final_output=SearchQuery(query='DeepSeek最新新闻')), '[{"title": "DeepSeek等中製AI模型操弄資訊、洩個資國安局", "link": "https://www.cna.com.tw/news/aipl/202511160037.aspx", "snippet": "國安局今天表示，依法抽驗包括DeepSeek等5款中製「生成式AI語言模型」後，所生成內容出現中共官宣、歷史偏差、資訊操弄等嚴重偏頗與不實資訊， ...", "date": "3 days ago", "position": 1}]']}
-----------------
[{"title": "DeepSeek等中製AI模型操弄資訊、洩個資國安局", "link": "https://www.cna.com.tw/news/aipl/202511160037.aspx", "snippet": "國安局今天表示，依法抽驗包括DeepSeek等5款中製「生成式AI語言模型」後，所生成內容出現中共官宣、歷史偏差、資訊操弄等嚴重偏頗與不實資訊， ...", "date": "3 days ago", "position": 1}]
{'messages': [HumanMessage(content='Beijing的天气怎么样？', additional_kwargs={}, response_metadata={})]}
-----------------
{'messages': [HumanMessage(content='Beijing的天气怎么样？', additional_kwargs={}, response_metadata={}), FinalResponse(final_output=WeatherLoc(location='Beijing'))]}
-----------------
工具执行结果: 北京的温度是16度，天气晴朗。
-----------------
{'messages': [HumanMessage(content='Beijing的天气怎么样？', additional_kwargs={}, response_metadata={}), FinalResponse(final_output=WeatherLoc(location='Beijing')), '北京的温度是16度，天气晴朗。']}
-----------------
北京的温度是16度，天气晴朗。
{'messages': [HumanMessage(content='我是Hygen，今年18，电话号是01000721，邮箱是ciallo@qq.com', additional_kwargs={}, response_metadata={})]}
-----------------
{'messages': [HumanMessage(content='我是Hygen，今年18，电话号是01000721，邮箱是ciallo@qq.com', additional_kwargs={}, response_metadata={}), FinalResponse(final_output=UserInfo(name='Hygen', age=18, email='ciallo@qq.com', phone='01000721'))]}
-----------------
2025-11-19 13:04:06,678 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2025-11-19 13:04:06,681 INFO sqlalchemy.engine.Engine INSERT INTO users (name, age, email, phone) VALUES (%(name)s, %(age)s, %(email)s, %(phone)s)
2025-11-19 13:04:06,681 INFO sqlalchemy.engine.Engine [generated in 0.00024s] {'name': 'Hygen', 'age': 18, 'email': 'ciallo@qq.com', 'phone': '01000721'}
2025-11-19 13:04:06,696 INFO sqlalchemy.engine.Engine COMMIT
工具执行结果: 用户信息已成功插入数据库。
-----------------
{'messages': [HumanMessage(content='我是Hygen，今年18，电话号是01000721，邮箱是ciallo@qq.com', additional_kwargs={}, response_metadata={}), FinalResponse(final_output=UserInfo(name='Hygen', age=18, email='ciallo@qq.com', phone='01000721')), '用户信息已成功插入数据库。']}
-----------------
用户信息已成功插入数据库。
</code></pre>
<h1>手动构建Tool Calling Agent的方法</h1>
<p>当然，工具调用的过程也可以手动进行实现，我们只需要进行适当的逻辑修改即可。</p>
<blockquote>
<p>前面的部分复用上面的代码即可，这里就不重复编写了</p>
</blockquote>
<h2>手动实现工具调用节点</h2>
<pre><code>from langchain_core.messages import ToolMessage
from langchain_openai import ChatOpenAI
import os

os.environ["OPENAI_API_KEY"] = "your-openai-api-key-here"  # 占位符：请换成你自己的密钥，切勿把真实密钥写进文章或仓库

llm = ChatOpenAI(
  base_url="https://api.deepseek.com/v1",
  model="deepseek-chat",
  temperature=0
)

def chat_with_model(state):
  """generate structured output"""
  messages = state['messages']
  response = llm.invoke(messages)
  return {"messages": [response]}

def execute_function(state):
  """execute tool calling"""
  tool_calls = state['messages'][-1].tool_calls
  results = []
  tools = [fetch_real_time_info, get_weather, insert_db]
  tools = {t.name: t for t in tools}
  for t in tool_calls:
    if not t['name'] in tools:
      result = "bad tool name, retry"
    else:
      result = tools[t['name']].invoke(t['args'])
    results.append(ToolMessage(tool_call_id=t['id'], name=t['name'], content=str(result)))
  return {"messages": results}
</code></pre>
<h2>判断是否需要工具调用</h2>
<p>定义路由函数来判断是否需要工具调用：</p>
<pre><code>def exists_function_calling(state: AgentState):
  result = state['messages'][-1]
  return len(result.tool_calls) &gt; 0
</code></pre>
<h2>添加自然语言响应节点</h2>
<p>在工具调用后，添加一个节点来生成最终的自然语言响应：</p>
<pre><code>SYSTEM_PROMPT = """
Please summarize the information obtained so far and generate a professional response. Note, please reply in Chinese.
"""

from langchain_core.messages import SystemMessage, HumanMessage
def natural_response(state):
  """generate final language responses"""
  messages = state['messages'][-1]
  messages = [SystemMessage(content=SYSTEM_PROMPT)] + [HumanMessage(content=messages.content)]
  response = llm.invoke(messages)
  return {"messages": [response]}
</code></pre>
<h2>完整的手动构建图结构</h2>
<pre><code>from typing import TypedDict, Annotated
import operator
from langchain_core.messages import AnyMessage
from langgraph.graph import StateGraph

class AgentState(TypedDict):
  messages: Annotated[list[AnyMessage], operator.add]

graph = StateGraph(AgentState)

graph.add_node("chat_with_model", chat_with_model)
graph.add_node("execute_function", execute_function)
graph.add_node("final_answer", final_answer)
graph.add_node("natural_response", natural_response)

graph.set_entry_point("chat_with_model")

graph.add_conditional_edges(
  "chat_with_model",
  exists_function_calling,
  {True: "execute_function", False: "final_answer"}
)

graph.add_edge("execute_function", "natural_response")
graph.add_edge("final_answer", "natural_response")

graph.set_finish_point("natural_response")

graph = graph.compile()
</code></pre>
<h2>使用绑定工具的大模型</h2>
<p>确保大模型绑定了工具：</p>
<pre><code>tools = [insert_db, fetch_real_time_info, get_weather]
llm = llm.bind_tools(tools)
</code></pre>
<h2>测试</h2>
<pre><code># 测试1: 普通对话（不需要工具）
query = "你好，请你介绍一下你自己"
input_message = {"messages": [HumanMessage(content=query)]}
result = graph.invoke(input_message)
print("-----------------")
print(result)

# 测试2: 网络搜索（需要工具）
query = "帮我查一下DeepSeek的最新新闻"
input_message = {"messages": [HumanMessage(content=query)]}
result = graph.invoke(input_message)
print("-----------------")
print(result)

# 测试3: 天气查询（需要工具）
query = "Beijing的天气怎么样？"
input_message = {"messages": [HumanMessage(content=query)]}
result = graph.invoke(input_message)
print("-----------------")
print(result)

# 测试4: 数据库插入（需要工具）
query = "我是Hygen，今年28，电话号是01000721，邮箱是ciallo@qq.com"
input_message = {"messages": [HumanMessage(content=query)]}
result = graph.invoke(input_message)
print("-----------------")
print(result)
</code></pre>
<p>实验结果</p>
<pre><code>-----------------
{'messages': [HumanMessage(content='你好，请你介绍一下你自己', additional_kwargs={}, response_metadata={}), AIMessage(content='你好！我是一个AI助手，很高兴认识你！我可以帮助你处理各种任务，包括：\n\n- 获取实时网络信息\n- 查询天气情况\n- 管理用户信息（存储到数据库中）\n\n我可以通过工具来获取最新的网络信息、查询特定城市的天气，以及将用户信息（姓名、年龄、邮箱、电话）保存到数据库中。\n\n请告诉我你需要什么帮助，我会尽力为你提供支持！', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 84, 'prompt_tokens': 368, 'total_tokens': 452, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 320}, 'prompt_cache_hit_tokens': 320, 'prompt_cache_miss_tokens': 48}, 'model_name': 'deepseek-chat', 'system_fingerprint': 'fp_ffc7281d48_prod0820_fp8_kvcache', 'id': '94aaa2f7-b65e-458c-b369-a70d841f105e', 'service_tier': None, 'finish_reason': 'stop', 'logprobs': None}, id='run--ee81ac9d-e4f0-492c-95db-1928d9c54202-0', usage_metadata={'input_tokens': 368, 'output_tokens': 84, 'total_tokens': 452, 'input_token_details': {'cache_read': 320}, 'output_token_details': {}}), AIMessage(content='你好！我是一个AI助手，很高兴认识你！我可以帮助你处理各种任务，包括：\n\n- 获取实时网络信息\n- 查询天气情况\n- 管理用户信息（存储到数据库中）\n\n我可以通过工具来获取最新的网络信息、查询特定城市的天气，以及将用户信息（姓名、年龄、邮箱、电话）保存到数据库中。\n\n请告诉我你需要什么帮助 ，我会尽力为你提供支持！', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 84, 'prompt_tokens': 368, 'total_tokens': 452, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 320}, 'prompt_cache_hit_tokens': 320, 'prompt_cache_miss_tokens': 48}, 'model_name': 'deepseek-chat', 'system_fingerprint': 'fp_ffc7281d48_prod0820_fp8_kvcache', 'id': '94aaa2f7-b65e-458c-b369-a70d841f105e', 'service_tier': None, 'finish_reason': 'stop', 'logprobs': None}, id='run--ee81ac9d-e4f0-492c-95db-1928d9c54202-0', usage_metadata={'input_tokens': 368, 'output_tokens': 84, 'total_tokens': 452, 'input_token_details': {'cache_read': 320}, 'output_token_details': {}}), AIMessage(content='你好！很高兴认识你！感谢你详细介绍你的功能和服务范围。\n\n根据你的描述，你确实具备以下强大的能力：\n\n**主要功能：**\n1. **实时网络信息查询** - 可以获取最新的网络资讯和信息\n2. **天气查询** - 能够查询特定城市的天气情况\n3. **用户信息管理** - 可以将用户信息（姓名、年龄、邮箱、电话）存储到数据库中\n\n**使用方式：**\n- 如果你需要查询任何实时信息，我可以帮你搜索网络\n- 如果你想知道某个城市的天气，告诉 我城市名即可\n- 如果你需要保存用户信息，请提供姓名、年龄、邮箱和电话\n\n我现在已经准备好为你提供帮助了！请告诉我你需要什么服务，或者有什么具体的问题需要解决？我会根据你的需求选择最合适的工具来协助你。', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 170, 'prompt_tokens': 468, 'total_tokens': 638, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 0}, 'prompt_cache_hit_tokens': 0, 'prompt_cache_miss_tokens': 468}, 'model_name': 'deepseek-chat', 'system_fingerprint': 'fp_ffc7281d48_prod0820_fp8_kvcache', 'id': '8f3dbeff-b403-4aef-a685-1bc425d6c62a', 'service_tier': None, 'finish_reason': 'stop', 'logprobs': None}, id='run--092e834b-2c6c-4d8a-a5ea-b4fb2df4f761-0', usage_metadata={'input_tokens': 468, 'output_tokens': 170, 'total_tokens': 638, 'input_token_details': {'cache_read': 0}, 'output_token_details': {}})]}
-----------------
{'messages': [HumanMessage(content='帮我查一下DeepSeek的最新新闻', additional_kwargs={}, response_metadata={}), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_00_EM6RKCZJbx09rIEDrVA0Re0L', 'function': {'arguments': '{"query": "DeepSeek最新新闻"}', 'name': 'fetch_real_time_info'}, 'type': 'function', 'index': 0}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 19, 'prompt_tokens': 371, 'total_tokens': 390, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 320}, 'prompt_cache_hit_tokens': 320, 'prompt_cache_miss_tokens': 51}, 'model_name': 'deepseek-chat', 'system_fingerprint': 'fp_ffc7281d48_prod0820_fp8_kvcache', 'id': 'a1826f70-cfa1-4fab-8412-09bc3f901a06', 'service_tier': None, 'finish_reason': 'tool_calls', 'logprobs': None}, id='run--a95bf745-8601-4fe5-bc77-4e2014e508cf-0', tool_calls=[{'name': 'fetch_real_time_info', 'args': {'query': 'DeepSeek最新新闻'}, 'id': 'call_00_EM6RKCZJbx09rIEDrVA0Re0L', 'type': 'tool_call'}], usage_metadata={'input_tokens': 371, 'output_tokens': 19, 'total_tokens': 390, 'input_token_details': {'cache_read': 320}, 'output_token_details': {}}), ToolMessage(content='[{"title": "DeepSeek | 世界新聞網", "link": "https://www.worldjournal.com/search/tagging/8877/DeepSeek", "snippet": "FT：DeepSeek新模型延後問世問題出在華為晶片還是得靠輝達 · 2025-08-14 03:00 ; OpenAI最新模型GPT-5發布傳DeepSeek將推R2 · 2025-08-14 02:04 ; 受惠DeepSeek-R2將發表中國AI ...", "position": 1}]', name='fetch_real_time_info', tool_call_id='call_00_EM6RKCZJbx09rIEDrVA0Re0L'), AIMessage(content='根据您提供的信息，我看到了关于DeepSeek的一些最新新闻摘要。这些信息主要来自世界新聞網，涉及以下几个要点：\n\n1. **DeepSeek新模型延后问世** - 有报道讨论其延迟原因是否与华为芯片相关还是需要依赖辉达（NVIDIA）\n\n2. **OpenAI GPT-5发布** - 同时有传闻DeepSeek将推出R2模型\n\n3. **中国AI受益** - 提到受惠于DeepSeek-R2即将发表\n\n不过，这些信息相对有限，主要是新闻标题和简短摘要。如果您需要更详细、更全面的DeepSeek相关信息，我可以 帮您搜索最新的实时信息来获取更完整的报道内容。\n\n您希望我为您搜索更多关于DeepSeek的详细资讯吗？', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 162, 'prompt_tokens': 509, 'total_tokens': 671, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 320}, 'prompt_cache_hit_tokens': 320, 'prompt_cache_miss_tokens': 189}, 'model_name': 'deepseek-chat', 'system_fingerprint': 'fp_ffc7281d48_prod0820_fp8_kvcache', 'id': '0f5dd1f4-4b97-41a8-bd61-81943a5da6bb', 'service_tier': None, 'finish_reason': 'stop', 'logprobs': None}, id='run--19b47710-a7fb-4d37-9d7d-07b58c48f4e5-0', usage_metadata={'input_tokens': 509, 'output_tokens': 162, 'total_tokens': 671, 'input_token_details': {'cache_read': 320}, 'output_token_details': {}})]}
-----------------
{'messages': [HumanMessage(content='Beijing的天气怎么样？', additional_kwargs={}, response_metadata={}), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_00_QxjiQuICuIOKzOOgQaJCAcGo', 'function': {'arguments': '{"location": "Beijing"}', 'name': 'get_weather'}, 'type': 'function', 'index': 0}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 368, 'total_tokens': 382, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 320}, 'prompt_cache_hit_tokens': 320, 'prompt_cache_miss_tokens': 48}, 'model_name': 'deepseek-chat', 'system_fingerprint': 'fp_ffc7281d48_prod0820_fp8_kvcache', 'id': '16c53fc1-d392-4626-8268-e6104fab5bae', 'service_tier': None, 'finish_reason': 'tool_calls', 'logprobs': None}, id='run--76f8f809-a413-48fb-8a30-9401700c3fd3-0', tool_calls=[{'name': 'get_weather', 'args': {'location': 'Beijing'}, 'id': 'call_00_QxjiQuICuIOKzOOgQaJCAcGo', 'type': 'tool_call'}], usage_metadata={'input_tokens': 368, 'output_tokens': 14, 'total_tokens': 382, 'input_token_details': {'cache_read': 320}, 'output_token_details': {}}), ToolMessage(content='北京的温度是16度，天气晴朗。', name='get_weather', tool_call_id='call_00_QxjiQuICuIOKzOOgQaJCAcGo'), AIMessage(content='根据您提供的信息，北京当前的温度是16度，天气晴朗。这是一个非常宜人的天气状况，适合户外活动和出行。\n\n如果您需要获取更详细的天气信息，比如湿度、风速、空气质量指 数等，我可以帮您查询最新的实时天气数据。您是否需要我为您获取更全面的北京天气信息？', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 64, 'prompt_tokens': 393, 'total_tokens': 457, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 320}, 'prompt_cache_hit_tokens': 320, 'prompt_cache_miss_tokens': 73}, 'model_name': 'deepseek-chat', 'system_fingerprint': 'fp_ffc7281d48_prod0820_fp8_kvcache', 'id': 'bfd56895-dda9-4e7e-9a48-99e105c185e1', 'service_tier': None, 'finish_reason': 'stop', 'logprobs': None}, id='run--bc232be5-2b56-44f5-801c-454435c722ab-0', usage_metadata={'input_tokens': 393, 'output_tokens': 64, 'total_tokens': 457, 'input_token_details': {'cache_read': 320}, 'output_token_details': {}})]}
2025-11-19 13:27:10,184 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2025-11-19 13:27:10,187 INFO sqlalchemy.engine.Engine INSERT INTO users (name, age, email, phone) VALUES (%(name)s, %(age)s, %(email)s, %(phone)s)
2025-11-19 13:27:10,189 INFO sqlalchemy.engine.Engine [generated in 0.00027s] {'name': 'Hygen', 'age': 28, 'email': 'ciallo@qq.com', 'phone': '01000721'}
2025-11-19 13:27:10,191 INFO sqlalchemy.engine.Engine COMMIT
-----------------
{'messages': [HumanMessage(content='我是Hygen，今年28，电话号是01000721，邮箱是ciallo@qq.com', additional_kwargs={}, response_metadata={}), AIMessage(content='您好Hygen！我已经收到了您的个人信息：\n- 姓名：Hygen\n- 年龄：28岁\n- 电话：01000721\n- 邮箱：ciallo@qq.com\n\n现在我将把这些信息保 存到数据库中。', additional_kwargs={'tool_calls': [{'id': 'call_00_svoG4YILmEjp7UVtyhHAm7CL', 'function': {'arguments': '{"name": "Hygen", "age": 28, "email": "ciallo@qq.com", "phone": "01000721"}', 'name': 'insert_db'}, 'type': 'function', 'index': 0}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 87, 'prompt_tokens': 384, 'total_tokens': 471, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 320}, 'prompt_cache_hit_tokens': 320, 'prompt_cache_miss_tokens': 64}, 'model_name': 'deepseek-chat', 'system_fingerprint': 'fp_ffc7281d48_prod0820_fp8_kvcache', 'id': '2b5d2240-b1eb-4cff-8ea6-642647c7980c', 'service_tier': None, 'finish_reason': 'tool_calls', 'logprobs': None}, id='run--625e59b0-ec90-455b-94b3-d210c882e61a-0', tool_calls=[{'name': 'insert_db', 'args': {'name': 'Hygen', 'age': 28, 'email': 'ciallo@qq.com', 'phone': '01000721'}, 'id': 'call_00_svoG4YILmEjp7UVtyhHAm7CL', 'type': 'tool_call'}], usage_metadata={'input_tokens': 384, 'output_tokens': 87, 'total_tokens': 471, 'input_token_details': {'cache_read': 320}, 'output_token_details': {}}), ToolMessage(content='用户信息已成功插入数据库。', name='insert_db', tool_call_id='call_00_svoG4YILmEjp7UVtyhHAm7CL'), AIMessage(content='根据您提供的信息，用户数据已成功保存到数据库中。这是一个标准的确认信息，表明：\n\n**操作结果：成功**\n- 用户信息已完整录入数据库系统\n- 数据存储过程顺利完成\n- 系统已确认保存 操作\n\n如果您需要：\n- 查询已保存的用户信息\n- 进行其他数据库操作\n- 获取更多关于数据管理的帮助\n\n请随时告知，我将为您提供相应的支持。', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 88, 'prompt_tokens': 391, 'total_tokens': 479, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 320}, 'prompt_cache_hit_tokens': 320, 'prompt_cache_miss_tokens': 71}, 'model_name': 'deepseek-chat', 'system_fingerprint': 'fp_ffc7281d48_prod0820_fp8_kvcache', 'id': '2d284b4f-9eec-49e1-91d2-8e51fb3f94da', 'service_tier': None, 'finish_reason': 'stop', 'logprobs': None}, id='run--ceaa01d6-396c-4bf6-8cf4-4644f265ffa6-0', usage_metadata={'input_tokens': 391, 'output_tokens': 88, 'total_tokens': 479, 'input_token_details': {'cache_read': 320}, 'output_token_details': {}})]}
</code></pre>
<h1>核心要点总结</h1>
<h2>Tool Calling Agent的优势</h2>
<ol>
<li><strong>自主工具选择</strong>：大模型可以根据用户输入自动判断需要调用哪个工具</li>
<li><strong>多工具支持</strong>：可以同时支持多个工具，大模型可以并行调用多个工具</li>
<li><strong>灵活扩展</strong>：可以轻松添加新工具，只需定义函数并添加<code>@tool</code>装饰器</li>
<li><strong>结构化输出</strong>：结合<code>Pydantic</code>模型，可以实现结构化的工具参数定义</li>
</ol>
<h2>使用注意事项</h2>
<ol>
<li><strong>Router Function的分支判断</strong>：需要正确设计路由逻辑，区分是否需要工具调用</li>
<li><strong>工具参数验证</strong>：因为调用工具的参数是由大模型根据用户输入的自然语言生成的，所以一定会存在尝试调用不存在的工具，或者无法返回与请求的架构匹配的参数等边缘情况，会直接导致整个图的运行中断</li>
<li><strong>错误处理</strong>：需要在工具调用节点中添加适当的错误处理机制</li>
<li><strong>状态管理</strong>：确保状态中包含消息列表，且最后一条消息是带有<code>tool_calls</code>的<code>AIMessage</code></li>
</ol>
<h2>与Router Agent的关系</h2>
<p><code>Tool Calling Agent</code>是在<code>Router Agent</code>基础上的扩展：</p>
<ul>
<li><strong>Router Agent</strong>：只能控制单个决策，选择执行哪个节点</li>
<li><strong>Tool Calling Agent</strong>：在某个条件分支中，大模型可以自主选择并使用多种工具</li>
</ul>
<p>这种设计使得<code>Tool Calling Agent</code>具有更高的控制级别，能够处理更复杂的任务场景。</p>
<h1>进阶优化方向</h1>
<p>对于工具调用而言，因为调用工具的参数是由大模型根据用户输入的自然语言生成的，所以一定会存在以下边缘情况：</p>
<ol>
<li><strong>调用不存在的工具</strong>：大模型可能尝试调用未定义的工具</li>
<li><strong>参数不匹配</strong>：工具调用参数可能无法与请求的架构匹配</li>
<li><strong>工具执行失败</strong>：工具执行过程中可能出现异常</li>
</ol>
<p>这些情况会直接导致整个图的运行中断。需要在后续的学习中关注错误处理和重试机制的实现。</p>
]]></content>
    <author><name>Hygen</name></author>
    <category term="Agent"/>
  </entry>
  <entry>
    <title>LangGraph的Router路由</title>
    <link href="https://www.hygen.red/posts/agent/langgraph/%E5%8E%9F%E7%90%86/004router%E8%B7%AF%E7%94%B1/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/agent/langgraph/%E5%8E%9F%E7%90%86/004router%E8%B7%AF%E7%94%B1/</id>
    <published>2025-11-17T00:00:00.000Z</published>
    <updated>2025-11-17T00:00:00.000Z</updated>
    <summary>LangGraph框架中Router路由代理的核心概念、实现机制和应用方法详解，包括条件边、结构化输出和路由图构建</summary>
    <content type="html"><![CDATA[<h1>LangGraph中的Router使用场景</h1>
<h2>1. Router的基本概念</h2>
<p>在<code>LangGraph</code>中，我们可以利用"条件边"这一概念来指导或约束大模型在处理特定任务时的逻辑流程。这种机制允许大模型在达到某一环节并满足预设条件时，根据不同的条件输出或数据，选择性地执行不同的逻辑路径。</p>
<p><img src="https://muyu001.oss-cn-beijing.aliyuncs.com/img/20241024002.png" alt="条件边路由机制" /></p>
<p>为了管理这样复杂的图结构，<code>LangGraph</code>使用的是一个类似于 <code>if-else</code>语句的结构组件，称为<code>Router</code>（路由）。这个组件允许大模型从一组预设的选项中选择合适的步骤来进行执行。</p>
<h3>1.1 简单边 vs 条件边</h3>
<p>对于简单的直接从节点<code>A</code>到节点<code>B</code>，我们一直使用的是<code>add_edge</code>方法：</p>
<pre><code>from langgraph.graph import START, StateGraph, END

def node_a(state):
    return {"x": state["x"] + 1}

def node_b(state):
    return {"x": state["x"] - 2}

builder = StateGraph(dict)

builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)

# 构建节点之间的边
builder.add_edge(START, "node_a")
builder.add_edge("node_a", "node_b")
builder.add_edge("node_b", END)

graph = builder.compile()
</code></pre>
<p>如果想选择性地路由到 1 个或多个边，则需要使用<code>add_conditional_edges</code>方法。该方法在<code>Graph</code>的基类中进行了定义：</p>
<pre><code>def add_conditional_edges(
    self,
    source: str,    # 起始节点
    path: Union[    # 这是一个可调用对象，其返回值决定接下来执行的节点
        Callable[..., Union[Hashable, list[Hashable]]],
        Callable[..., Awaitable[Union[Hashable, list[Hashable]]]],
        Runnable[Any, Union[Hashable, list[Hashable]]],
    ],
    path_map: Optional[Union[dict[Hashable, str], list[str]]] = None,  # 路径到节点名称的可选映射
    then: Optional[str] = None,  # 在path选择的节点之后执行的节点的名称
) -&gt; Self:
</code></pre>
<h3>1.2 路由函数的使用</h3>
<p>路由函数<code>routing_function</code>接受图的当前<code>state</code>并返回一个值，根据返回值的不同，来决定路由到哪个节点：</p>
<pre><code>def routing_function(state):
    if state["x"] == 10:
        return "node_b"
    else:
        return "node_c"

builder.add_conditional_edges("node_a", routing_function)
</code></pre>
<p>默认情况下，<code>routing_function</code>路由函数的返回值用作将状态发送到下一个节点的名称。除此之外，还可以使用<code>path_map</code>参数，通过一个字典的数据结构将<code>routing_function</code>的输出映射到下一个节点的名称：</p>
<pre><code>def routing_function(state):
    if state["x"] == 10:
        return True
    else:
        return False

builder.add_conditional_edges(
    "node_a", 
    routing_function, 
    {True: "node_b", False: "node_c"}
)
</code></pre>
<h2>2. Router的实际应用场景</h2>
<p>一般来说，<code>Agent</code>是可以接收各种形式的输入，并通过预设的路由逻辑来确定执行的具体操作。如图所示，<code>Agent</code>的开始节点（Start）接收输入数据，这些输入可以是查询请求（例如"name: muyu, age: 18, phone: 123"或"Hello"）。根据输入的不同，流程通过<code>Router</code>函数进行决策，将不同的输入引导到正确的处理流程。</p>
<p><img src="https://muyu001.oss-cn-beijing.aliyuncs.com/img/20241024003.png" alt="Router应用场景" /></p>
<p>这里的核心是<code>Router function</code>，它根据输入数据的结构和内容，动态地决定下一步应该执行的节点。例如，对于具体的查询请求，<code>Router</code>决定需要访问数据库（Mysql节点），而对于简单的问候（如"Hello"），则直接返回一个响应（Response节点）。</p>
<p><strong>关键要点：</strong></p>
<ul>
<li>在构建实际的<code>Agent</code>时，<code>Router function</code>的定义才是最关键且最重要的</li>
<li>我们需要在这个函数中，基于特定的一些格式或者标识来区分该执行哪一条分支的逻辑</li>
<li>对于消息的传递，大模型往往是通过结构化输出，引导其在响应的过程中应遵循哪种模式来工作，就类似于工具调用过程</li>
<li><code>Router</code>就很好的利用到了这个特性，通过结构化输出的特性来控制接下来的分支路径</li>
</ul>
<h2>3. 结构化输出的实现方式</h2>
<p>在<code>LangGraph</code>中，实现结构化输出可以通过以下三种有效方式完成：</p>
<ol>
<li><strong>提示工程</strong>：指示大模型以特定格式做出回应</li>
<li><strong>输出解析器</strong>：采用后处理的方法从大模型的响应中提取结构化数据</li>
<li><strong>工具调用</strong>：利用一些内置工具调用功能来生成结构化输出</li>
</ol>
<h3>3.1 提示工程方法</h3>
<p>直接通过提示工程让大模型生成特定格式的输出：</p>
<pre><code>from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o-mini")

prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            "Answer the user query. Wrap the output in `json`",
        ),
        ("human", "{query}"),
    ]
)

chain = prompt | llm
ans = chain.invoke({"query": "我叫Hygen，今年18岁，邮箱地址是ciallo@gmial.com，电话是01000721"})
</code></pre>
<p>直接通过提示工程让大模型生成特定格式的输出虽然是可行的，但这种方法在复杂的<code>Agent</code>构建流程中非常并不稳定。</p>
<h3>3.2 提示工程 + 输出解析器</h3>
<p>引入后处理步骤，通过输出解析器来格式化大模型生成的响应，可以提高输出的准确性和一致性：</p>
<pre><code>from langchain_core.messages import AIMessage
import json
import re
from typing import List

def extract_json(message: AIMessage) -&gt; List[dict]:
    """Extracts JSON content from a string where JSON is embedded between ```json and ``` tags."""
    text = message.content
    pattern = r"\`\`\`json(.*?)\`\`\`"
    matches = re.findall(pattern, text, re.DOTALL)
    try:
        return [json.loads(match.strip()) for match in matches]
    except Exception:
        raise ValueError(f"Failed to parse: {message}")

chain = prompt | llm | extract_json
ans = chain.invoke({"query": "我叫Hygen，今年18岁，邮箱地址是ciallo@gmial.com，电话是01000721"})
</code></pre>
<h3>3.3 内置工具方法（推荐）</h3>
<p>在<code>LangGraph</code>中我们更常用的，且效果更好的是，直接使用其内置的工具方法：<code>.with_structured_output()</code>。</p>
<p>这个方法通过接受一个定义了所需输出属性的名称、类型和描述的模式作为输入，进而生成一个类似模型的 <code>Runnable</code>。不同于常规模型输出字符串或消息，这个 <code>Runnable</code> 输出一个与输入模式相匹配的对象。</p>
<p>可以通过几种方式指定这种架构，包括 <code>TypedDict</code> 类、<code>JSON Schema</code> 或 <code>Pydantic</code> 类。如果采用 <code>TypedDict</code> 或 <code>JSON Schema</code>，<code>Runnable</code> 将输出一个字典；若使用 <code>Pydantic</code> 类，则输出一个 <code>Pydantic</code> 对象。</p>
<h2>4. 使用Pydantic做结构化输出</h2>
<p>使用<code>Pydantic</code>去限定输出格式，可以确保所有通过此模型处理的数据都会符合指定的结构和数据类型，从而减少数据处理中的错误并增加代码的健壮性。此外，Pydantic的验证系统还会自动确保所有字段都符合预定义的格式，如果输入数据不符合预期，则会抛出错误。</p>
<h3>4.1 定义Pydantic模型</h3>
<pre><code>from typing import Optional
from pydantic import BaseModel, Field

# 定义 Pydantic 模型
class UserInfo(BaseModel):
    """Extracted user information, such as name, age, email, and phone number, if relevant."""
    name: str = Field(description="The name of the user")
    age: Optional[int] = Field(description="The age of the user")
    email: str = Field(description="The email address of the user")
    phone: Optional[str] = Field(description="The phone number of the user")
</code></pre>
<p>在这个<code>UserInfo</code>模型中：</p>
<ul>
<li><code>name</code>（必需）: 存储用户的名字</li>
<li><code>age</code>（可选）: 存储用户的年龄，这是一个可选字段</li>
<li><code>email</code>（必需）: 存储用户的电子邮件地址</li>
<li><code>phone</code>（可选）: 存储用户的电话号码，这也是一个可选字段</li>
</ul>
<h3>4.2 使用with_structured_output</h3>
<p>对于<code>.with_structured_output()</code>方法，如果我们希望模型返回一个 <code>Pydantic</code> 对象，只需要传入所需的 <code>Pydantic</code> 类即可：</p>
<pre><code>import os
from langchain_openai import ChatOpenAI

os.environ["OPENAI_API_KEY"] = "sk-xxx" # 更换为自己的api-key

llm = ChatOpenAI(
  base_url="https://api.deepseek.com/v1",
  model="deepseek-chat",
  temperature=0
)

# 使用 function_calling 方法，兼容更多模型
structured_llm = llm.with_structured_output(UserInfo, method="function_calling")
extracted_user_info = structured_llm.invoke("我叫Hygen，今年18岁，邮箱地址是ciallo@gmial.com，电话是01000721")
print(extracted_user_info)
</code></pre>
<p>运行结果：</p>
<pre><code>name='Hygen' age=18 email='ciallo@gmial.com' phone='01000721'
</code></pre>
<p>它返回的是一个<code>UserInfo</code>的<code>Pydantic</code>对象，每个字段中则填充了在原始非结构化文本中提取出来的结构化信息。</p>
<h3>4.3 在Router Function中使用</h3>
<p>经过这样的格式化输出，对于<code>Router function</code>中，我们就可以通过类似这样的伪代码去继续路由分支的选择：</p>
<pre><code>if isinstance(extracted_user_info, UserInfo):
    print("执行节点A的逻辑")
else:
    print("执行节点B的逻辑")
</code></pre>
<p>这就是结构化输出对于<code>LangGraph</code>中路由函数逻辑判断的意义所在。</p>
<h2>5. 使用TypedDict做结构化输出</h2>
<p>如果不想使用 <code>Pydantic</code>去明确地验证输出参数，则可以使用 <code>TypedDict</code> 类定义结构化输出的模式。这就可以使用特殊<code>Annotated</code>语法，添加对指定字段的默认值和描述：</p>
<pre><code>from typing import Optional
from typing_extensions import Annotated, TypedDict

# 定义 TypedDict 模型
class UserInfo(TypedDict):
    """Extracted user information from text"""
    name: Annotated[str, ..., "The user's name"]
    age: Annotated[Optional[int], None, "The user's age"]
    email: Annotated[str, ..., "The user's email address"]
    phone: Annotated[Optional[str], None, "The user's phone number"]

structured_llm = llm.with_structured_output(UserInfo, method="function_calling")
extracted_user_info = structured_llm.invoke("我叫Hygen，今年18岁，邮箱地址是ciallo@gmial.com，电话是01000721")
</code></pre>
<p>使用 <code>TypedDict</code> 创建的"对象"实际上是一个字典。它没有<code>Pydantic</code>模型那样的方法和属性，因此功能相对简单。<code>TypedDict</code> 主要用于静态类型检查，但它不会在运行时进行类型检查，但搭配着<code>LangGraph</code>中已实现的基本验证机制，也是一种不错的方法。</p>
<h2>6. 使用JSON Schema做结构化输出</h2>
<p>对于<code>Json Schema</code>格式大家应该最为熟悉，不需要导入或类，可以直接通过字典的形式清楚地准确记录每个参数，但代价是代码会更加冗长：</p>
<pre><code># 定义 JSON Schema
json_schema = {
    "title": "user_info",
    "description": "Extracted user information",
    "type": "object",
    "properties": {
        "name": {
            "type": "string",
            "description": "The user's name",
        },
        "age": {
            "type": "integer",
            "description": "The user's age",
            "default": None,
        },
        "email": {
            "type": "string",
            "description": "The user's email address",
        },
        "phone": {
            "type": "string",
            "description": "The user's phone number",
            "default": None,
        },
    },
    "required": ["name", "email"],
}

structured_llm = llm.with_structured_output(UserInfo, method="function_calling")
extracted_user_info = structured_llm.invoke("我叫Hygen，今年18岁，邮箱地址是ciallo@gmial.com，电话是01000721")
</code></pre>
<h2>7. 结合结构化输出构建路由图</h2>
<p>三种不同的结构化输出方法，我们更常使用的是用<code>Pydantic</code>来处理路由决策。在这种策略下，我们可以通过定义一个包含<code>Union</code>类型属性的父模型来灵活地从多种模式中选择适当的路由分支。</p>
<h3>7.1 定义Union类型的响应模型</h3>
<p>例如，如果我们想根据输出决定是查询数据库还是直接回答问题，可以创建一个统一的模型来封装可能的输出类型：</p>
<pre><code>from typing import Union, Optional
from pydantic import BaseModel, Field

# 定义数据库插入的用户信息模型
class UserInfo(BaseModel):
    """Extracted user information, such as name, age, email, and phone number, if relevant."""
    name: str = Field(description="The name of the user")
    age: Optional[int] = Field(description="The age of the user")
    email: str = Field(description="The email address of the user")
    phone: Optional[str] = Field(description="The phone number of the user")

# 定义正常生成模型回复的模型
class ConversationalResponse(BaseModel):
    """Respond to the user's query in a conversational manner. Be kind and helpful."""
    response: str = Field(description="A conversational response to the user's query")

# 定义最终响应模型，可以是用户信息或一般响应
class FinalResponse(BaseModel):
    final_output: Union[UserInfo, ConversationalResponse]
</code></pre>
<p>这个扩展后的代码将用于提取和存储用户的基本信息的<code>UserInfo</code>模型与用于生成面向用户的交流响应的<code>ConversationalResponse</code>模型统一的放在了<code>FinalResponse</code>模型中，使用<code>Union</code>类型来支持灵活的输出选项。</p>
<h3>7.2 构建完整的路由图</h3>
<h4>7.2.1 定义节点函数</h4>
<p>首先，定义<code>Pydantic</code>模型以及用于生成格式化输出的大模型实例：</p>
<pre><code>import os
from langchain_openai import ChatOpenAI

os.environ["OPENAI_API_KEY"] = "sk-xxx" # 输入自己的api-key

llm = ChatOpenAI(
  base_url="https://api.deepseek.com/v1",
  model="deepseek-chat",
  temperature=0
)
</code></pre>
<p>定义路由节点函数，将用户输入的文本转化成格式化输出：</p>
<pre><code>def chat_with_model(state):
  """generate structured output"""
  messages = state['messages']
  structured_llm = llm.with_structured_output(FinalResponse, method="function_calling")
  response = structured_llm.invoke(messages)
  return {"messages": [response]}
</code></pre>
<p>连接数据库并创建表</p>
<pre><code>from sqlalchemy import create_engine, Table, Column, Integer, String, MetaData, text
from sqlalchemy.orm import declarative_base, sessionmaker


# 创建基类
Base = declarative_base()

# 定义 UserInfo 模型
class User(Base):
  __tablename__ = 'users'
  id = Column(Integer, primary_key=True)
  name = Column(String(50))
  age = Column(Integer)
  email = Column(String(100))
  phone = Column(String(15))

# 数据库连接配置
# 格式：mysql+pymysql://用户名:密码@主机:端口/数据库名?charset=utf8mb4
# 本地连接说明：
# - 用户名：你的 MySQL 用户名（默认通常是 root）
# - 密码：你的 MySQL 密码
# - 主机：localhost 或 127.0.0.1（本地连接）
# - 端口：3306（MySQL 默认端口，如果使用默认端口可以省略）
# - 数据库名：要连接的数据库名称
# - charset=utf8mb4：设置字符集为 utf8mb4，支持更广泛的 Unicode 字符

# MySQL 连接信息（不包含数据库名，用于创建数据库）
MYSQL_USER = 'root'
MYSQL_PASSWORD = '123456'
MYSQL_HOST = 'localhost'
MYSQL_PORT = '3306'
DATABASE_NAME = 'langgraph_agent'

# 先连接到 MySQL 服务器（不指定数据库），用于创建数据库
server_uri = f'mysql+pymysql://{MYSQL_USER}:{MYSQL_PASSWORD}@{MYSQL_HOST}:{MYSQL_PORT}?charset=utf8mb4'
server_engine = create_engine(server_uri, echo=False)

# 创建数据库（如果不存在）
with server_engine.connect() as conn:
    conn.execute(text(f"CREATE DATABASE IF NOT EXISTS `{DATABASE_NAME}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"))
    conn.commit()

# 连接到指定的数据库
DATABASE_URI = f'mysql+pymysql://{MYSQL_USER}:{MYSQL_PASSWORD}@{MYSQL_HOST}:{MYSQL_PORT}/{DATABASE_NAME}?charset=utf8mb4'
engine = create_engine(DATABASE_URI, echo=True)

# 如果表不存在，则创建表
Base.metadata.create_all(engine)

# 创建会话
Session = sessionmaker(bind=engine)
session = Session()
</code></pre>
<p>然后分别定义两个分支节点，其中<code>final_answer</code>用于直接生成响应，而<code>insert_db</code>用于执行数据库插入操作：</p>
<pre><code>def final_answer(state):
  """generate natural language responses"""
  messages = state['messages'][-1]
  response = messages.final_output.response
  return {"messages": [response]}

def insert_db(state):
  """Insert user information into the database"""
  session = Session() # 确保为每次操作创建新的会话
  try:
    result = state['messages'][-1]
    output = result.final_output
    user = User(name=output.name, age=output.age, email=output.email, phone=output.phone)
    session.add(user)
    session.commit()
    return {"messages": [f"数据已成功存储至Mysql数据库。"]}
  except Exception as e:
    session.rollback()  # 出错时回滚
    return {"messages": [f"数据存储失败，错误原因：{e}"]}
  finally:
    session.close()  # 关闭会话
</code></pre>
<h4>7.2.2 定义Router Function</h4>
<p>定义<code>generate_branch</code>函数作为<code>Router Function</code>，根据经过<code>chat_with_model</code>节点后产生的不同<code>Pydantic</code>对象，选择连接不同的节点：</p>
<pre><code>from langchain_core.messages import AnyMessage
from typing_extensions import TypedDict, Annotated
import operator

class AgentState(TypedDict):
  messages: Annotated[list[AnyMessage], operator.add]

def generate_branch(state: AgentState):
  result = state['messages'][-1]
  output = result.final_output
  if isinstance(output, UserInfo):
    return True
  elif isinstance(output, ConversationalResponse):
    return False
</code></pre>
<h4>7.2.3 构建图结构</h4>
<p>构建图并使用条件边来生成<code>Router</code>：</p>
<pre><code>from langgraph.graph import StateGraph

graph = StateGraph(AgentState)

graph.add_node("chat_with_model", chat_with_model)
graph.add_node("final_answer", final_answer)
graph.add_node("insert_db", insert_db)

graph.set_entry_point("chat_with_model")

graph.add_conditional_edges(
  "chat_with_model",
  generate_branch,
  {True: "insert_db", False: "final_answer"}
)

graph.set_finish_point("final_answer")
graph.set_finish_point("insert_db")

graph = graph.compile()
</code></pre>
<h4>7.2.4 测试路由图</h4>
<p>测试执行插入数据库的条件分支：</p>
<pre><code>from langchain_core.messages import HumanMessage

query = "我叫Hygen，今年18岁，邮箱地址是ciallo@gmial.com，电话是01000721"
input_message = {"messages": [HumanMessage(content=query)]}
result = graph.invoke(input_message)
print(result)
</code></pre>
<p>运行结果：</p>
<pre><code>{'messages': [HumanMessage(content='我叫Hygen，今年18岁，邮箱地址是ciallo@gmial.com，电话是01000721', additional_kwargs={}, response_metadata={}), FinalResponse(final_output=UserInfo(name='Hygen', age=18, email='ciallo@gmial.com', phone='01000721')), '数据已成功存储至Mysql数据库。']}
</code></pre>
<p>去查看本地数据库可以看到确实有新的数据插入：</p>
<p><img src="./8ff8ac3f-d72e-4995-a9d0-2a0096333c58.png" alt="数据库插入" /></p>
<p>而如果正常的问答，则会经过<code>final_answer</code>直接生成响应：</p>
<pre><code>query = "你好，请你介绍一下你自己"
input_message = {"messages": [HumanMessage(content=query)]}
result = graph.invoke(input_message)
print(result)
</code></pre>
<p>运行结果：</p>
<pre><code>{'messages': [HumanMessage(content='你好，请你介绍一下你自己', additional_kwargs={}, response_metadata={}), FinalResponse(final_output=ConversationalResponse(response='你好！我是一个AI助手，专门设计来帮助用户处理各种任务和回答问题。我可以协助您进行信息查询、文本处理、数据分析等
多种工作。如果您有任何需要帮助的地方，请随时告诉我，我会尽力为您提供支持！')), '你好！我是一个AI助手，专门设计来帮助用户处理各种任务和回答问题。我可以协助您进行信息查询、文本处理、数据分析等多种工作。如果您有任何需要帮助的地方，请随时告诉我，我会尽力为您提供支持！']}
</code></pre>
<h2>8. Router Agent的优势与局限</h2>
<h3>8.1 优势</h3>
<p><code>Router Agent</code>（路由代理）的优势就是可以精准的控制程序链路中的每一个细节，通过预定义的分支结构，可以根据用户的输入请求灵活适配不同的场景。</p>
<p>在这个过程中，结构化输出对于路由至关重要，因为它们确保系统可以可靠地解释大模型的决定并采取行动。</p>
<h3>8.2 局限</h3>
<p>同时，这也表现出来了这是一种相对有限的控制级别的代理架构，因为大模型通常只能控制单个决策。</p>
<p>想象一下上面的场景中，如果我们希望定义的<code>insert_db</code>不仅仅只是包含插入数据库，而是有一堆各式各样的工具，比如网络搜索，RAG等等，应该如何进一步的扩展呢？难道要做对每一个工具在<code>insert_db</code>节点下再通过<code>Router Function</code>做分支判断吗？虽然可行，但总归并不是高效的做法。</p>
<p>由此，我们接下来进一步介绍<code>Tool Calling Agent</code>（工具调用代理）来高效的解决这一问题。</p>
<h2>9. 核心要点总结</h2>
<h3>9.1 Router的核心机制</h3>
<ol>
<li><strong>条件边（Conditional Edges）</strong>：<code>LangGraph</code>使用<code>add_conditional_edges</code>方法实现条件路由</li>
<li><strong>路由函数（Router Function）</strong>：接受当前状态，返回下一个节点的标识</li>
<li><strong>路径映射（Path Map）</strong>：可选参数，将路由函数的返回值映射到节点名称</li>
</ol>
<h3>9.2 结构化输出的重要性</h3>
<ol>
<li><strong>三种实现方式</strong>：提示工程、输出解析器、工具调用（<code>.with_structured_output()</code>）</li>
<li><strong>三种模式</strong>：Pydantic、TypedDict、JSON Schema</li>
<li><strong>推荐使用</strong>：Pydantic + <code>.with_structured_output()</code>方法，效果最好且最稳定</li>
</ol>
<h3>9.3 路由图构建流程</h3>
<ol>
<li><strong>定义数据模型</strong>：使用Pydantic定义结构化输出模型</li>
<li><strong>创建节点函数</strong>：包括路由节点和分支节点</li>
<li><strong>定义Router Function</strong>：根据结构化输出判断路由方向</li>
<li><strong>构建图结构</strong>：使用<code>add_conditional_edges</code>添加条件边</li>
<li><strong>测试验证</strong>：使用不同输入测试路由逻辑</li>
</ol>
<h3>9.4 实践建议</h3>
<ol>
<li><strong>Router Function设计</strong>：这是最关键的部分，需要基于特定格式或标识来区分分支逻辑</li>
<li><strong>结构化输出</strong>：充分利用大模型的结构化输出特性来控制分支路径</li>
<li><strong>Union类型</strong>：使用Union类型支持多种输出模式，提高灵活性</li>
<li><strong>逐步扩展</strong>：从简单的路由开始，逐步添加复杂的分支逻辑</li>
</ol>
<h2>10. 学习要点</h2>
<p>通过本节的学习，需要掌握：</p>
<ol>
<li>✅ 理解Router的基本概念和条件边机制</li>
<li>✅ 掌握<code>add_conditional_edges</code>方法的使用</li>
<li>✅ 了解三种结构化输出的实现方式</li>
<li>✅ 熟练使用Pydantic、TypedDict、JSON Schema做结构化输出</li>
<li>✅ 能够结合结构化输出构建完整的路由图</li>
<li>✅ 理解Router Agent的优势与局限</li>
</ol>
<hr />
<p><strong>注意</strong>：Router是LangGraph中实现智能路由决策的核心机制，结构化输出是Router Function实现的关键技术。掌握这些内容，是构建复杂AI Agent应用的重要基础。</p>
]]></content>
    <author><name>Hygen</name></author>
    <category term="Agent"/>
  </entry>
  <entry>
    <title>Transformer专题01. Transformer与注意力机制入门</title>
    <link href="https://www.hygen.red/posts/nlp/transformer/001transformer%E4%B8%8E%E6%B3%A8%E6%84%8F%E5%8A%9B%E6%9C%BA%E5%88%B6%E5%85%A5%E9%97%A8/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/nlp/transformer/001transformer%E4%B8%8E%E6%B3%A8%E6%84%8F%E5%8A%9B%E6%9C%BA%E5%88%B6%E5%85%A5%E9%97%A8/</id>
    <published>2025-11-17T00:00:00.000Z</published>
    <updated>2025-11-17T00:00:00.000Z</updated>
    <summary>简要介绍Transformer与注意力机制</summary>
    <content type="html"><![CDATA[<h1>1. NLP是什么？</h1>
<p>NLP(Normal Language Process)即常见的自然语言处理，它是目前AI最热门的领域之一，旨在让计算机能够<strong>理解、解释和生成人类语言</strong>。</p>
<p>用更简单的话来说，NLP就是<strong>教计算机读懂、听懂并说人话</strong>的技术。</p>
<h2>NLP的两大核心任务</h2>
<ol>
<li>
<p><strong>自然语言理解</strong>：让计算机<strong>理解</strong>人类语言的含义。</p>
<ul>
<li><em>例子</em>：当你对智能音箱说“今天天气怎么样？”，它能理解你是在询问天气信息。</li>
</ul>
</li>
<li>
<p><strong>自然语言生成</strong>：让计算机根据信息<strong>生成</strong>通顺的人类语言文本。</p>
<ul>
<li><em>例子</em>：天气预报系统根据数据自动生成“今天晴转多云，气温25-30度”的文本。</li>
</ul>
</li>
</ol>
<p>NLP技术已经深入我们生活的方方面面，例如智能语音助手、智能客服机器人、机器翻译等。</p>
<h2>NLP核心技术简介</h2>
<ul>
<li><strong>词向量</strong>：将文字转换为计算机能处理的数字向量，同时保留语义信息。</li>
<li><strong>循环神经网络/Transformer模型</strong>：处理序列数据的强大模型，Transformer是目前的主流架构。</li>
<li><strong>预训练语言模型</strong>：如BERT、GPT系列等。这些模型在大量文本数据上进行预训练，学会了语言的通用规律，然后可以针对特定任务（如翻译、问答）进行微调，效果非常出色。</li>
</ul>
<h1>2. Transformer之前NLP的瓶颈？</h1>
<p>在Transformer出现之前，NLP领域的主流是基于<code>RNN/LSTM</code>的序列模型。它们虽然强大，但其顺序处理的天性导致了<strong>并行化能力差</strong>和<strong>长程依赖建模能力不足</strong>这两个核心瓶颈。</p>
<p>正是为了突破这些瓶颈，Transformer模型应运而生。 它通过<strong>自注意力机制</strong>和<strong>完全并行化</strong>的架构，一举解决了以上所有问题，成为了NLP领域真正的游戏规则改变者。</p>
<h1>3. Transformer的地位和发展历程</h1>
<h2><strong>Transformer的核心地位</strong></h2>
<ol>
<li>
<p><strong>基石模型</strong>：Transformer 不仅是众多最先进NLP模型的<strong>基础架构</strong>，更是整个 <strong>“大语言模型时代”的基石</strong>。没有Transformer，就没有今天的GPT、BERT、T5等系列模型。</p>
</li>
<li>
<p><strong>技术范式的革命</strong>：它通过 <strong>“自注意力机制”</strong> 完全取代了过去的RNN和CNN在序列建模中的核心地位，解决了<strong>长程依赖</strong>和<strong>并行化困难</strong>两大瓶颈。</p>
</li>
<li>
<p><strong>领域的统一者</strong>：Transformer的出现，使得NLP领域几乎所有任务（翻译、摘要、问答等）都可以使用同一种核心架构来解决，只是输入输出和微调方式不同，实现了技术路线的统一。</p>
</li>
<li>
<p><strong>边界的拓展者</strong>：其影响力已远超NLP，在<strong>计算机视觉、语音识别、生物信息学</strong>等领域都取得了巨大成功，成为了跨模态的通用 backbone 架构。</p>
</li>
</ol>
<hr />
<h2><strong>Transformer的发展历程</strong></h2>
<pre><code>timeline
    title Transformer 发展历程
    2017 : 原始Transformer诞生&lt;br&gt;开创自注意力机制
    2018 : GPT-1 &amp; BERT登场&lt;br&gt;开启预训练-微调范式
    2019-2020 : 规模竞赛开始&lt;br&gt;GPT-3引发范式革命
    2021-2022 : 多模态与&lt;br&gt;ChatGPT破圈
    2023至今 : 生成式AI爆发&lt;br&gt;AGI之路开启
</code></pre>
<h3><strong>关键节点详解</strong></h3>
<ul>
<li>
<p><strong>2017：诞生——《Attention is All You Need》</strong></p>
<ul>
<li>谷歌发布这篇里程碑式的论文，首次提出了完全基于自注意力机制的Transformer架构，初衷是为了更好地完成机器翻译任务。</li>
</ul>
</li>
<li>
<p><strong>2018：崛起——预训练模型的黎明</strong></p>
<ul>
<li><strong>GPT-1 (OpenAI)</strong>：首次展示了基于Transformer解码器的<strong>生成式预训练</strong> 的强大潜力。</li>
<li><strong>BERT (Google)</strong>：基于Transformer编码器，通过<strong>双向上下文理解</strong> 在11项NLP任务上刷新纪录，引爆了“预训练-微调”范式。</li>
</ul>
</li>
<li>
<p><strong>2019-2020： scaling law（规模扩展）与能力涌现</strong></p>
<ul>
<li><strong>GPT-2 &amp; GPT-3 (OpenAI)</strong>：沿着GPT的道路，模型参数从15亿暴增至1750亿。GPT-3展示了惊人的<strong>少样本/零样本学习能力</strong>，证明了“大力出奇迹”的可行性。</li>
<li><strong>T5 (Google)</strong> 等模型将所有NLP任务都统一为“文本到文本”的格式，进一步巩固了Transformer的通用性。</li>
</ul>
</li>
<li>
<p><strong>2021-2022： 多模态与“破圈”应用</strong></p>
<ul>
<li><strong>Vision Transformer (ViT)</strong>：证明Transformer无需CNN，直接在图像补丁上也能实现顶尖的计算机视觉效果。</li>
<li><strong>DALL·E, Stable Diffusion</strong> 等文生图模型，其核心也使用了Transformer来理解文本和生成图像。</li>
<li><strong>ChatGPT (2022年底)</strong>：基于GPT-3.5，通过指令微调和人类反馈强化学习，让Transformer模型具备了卓越的对话和指令遵循能力，引发全球性的AI热潮。</li>
</ul>
</li>
<li>
<p><strong>2023至今： 生成式AI的爆发与AGI的探索</strong></p>
<ul>
<li><strong>GPT-4、Llama、Claude</strong> 等模型群雄逐鹿，技术迭代飞速。</li>
<li>模型能力从纯文本扩展到<strong>语音、图像、视频</strong>的多模态理解与生成。</li>
<li>Transformer已成为探索通用人工智能的核心架构之一。</li>
</ul>
</li>
</ul>
<h1>4. 序列模型的基本思路与根本诉求</h1>
<p>序列数据是一种按照特定顺序排列的数据，如股票价格的历史记录、WiFi毫米波信号等。序列数据有着<strong>样本与样本相关联</strong>的特点，对时间序列数据而言，每个样本代表一个时间点，样本与样本之间的关联就是时间点与时间点之间的关联。而对于文本序列而言，每个字或词的语义信息就是关联的内容，要理解一个句子的含义，就必须理解样本与样本之间的关系。</p>
<p>序列算法的根本诉求是要建立样本与样本之间的关联，并借助这种关联提炼出对序列数据的理解。</p>
<p>要理解Transformer模型的本质，首先我们要回归到序列数据、序列模型这些基本概念上来。序列数据是一种按照特定顺序排列的数据，它在现实世界中无处不在，例如股票价格的历史记录、语音信号、文本数据、视频数据等等，主要是按照某种特定顺序排列、且该顺序不能轻易被打乱的数据都被称之为是序列数据。序列数据有着“样本与样本有关联”的特点；对时间序列数据而言，每个样本就是一个时间点，因此样本与样本之间的关联就是时间点与时间点之间的关联。对文字数据而言，每个样本就是一个字/一个词，因此样本与样本之间的关联就是字与字之间、词与词之间的语义关联。很显然，要理解一个时间序列的规律、要理解一个完整的句子所表达的含义，就必须要理解样本与样本之间的关系。</p>
<p>对于一般表格类数据，我们一般重点研究特征与标签之间的关联，但<strong>在序列数据中，众多的本质规律与底层逻辑都隐藏在其样本与样本之间的关联中</strong>，这让序列数据无法适用于一般的机器学习与深度学习算法。这是我们要创造专门处理序列数据的算法的根本原因。在深度学习与机器学习的世界中，<strong>序列算法的根本诉求是要建立样本与样本之间的关联，并借助这种关联提炼出对序列数据的理解</strong>。唯有找出样本与样本之间的关联、建立起样本与样本之间的根本联系，序列模型才能够对序列数据实现分析、理解和预测。</p>
<p>在机器学习和深度学习的世界当中，存在众多经典且有效的序列模型。这些模型通过如下的方式来建立样本与样本之间的关联</p>
<ul>
<li>ARIMA家族算法群</li>
</ul>
<blockquote>
<p>过去影响未来，因此未来的值由过去的值加权求和而成，以此构建样本与样本之间的关联。</p>
</blockquote>
<p>$$AR模型：y_t = c + w_1 y_{t-1} + w_2 y_{t-2} + \dots + w_p y_{t-p} + \varepsilon_t$$</p>
<ul>
<li>循环网络家族</li>
</ul>
<blockquote>
<p>遍历时间点/样本点，将过去的时间上的信息传递存储在中间变量中，传递给下一个时间点，以此构建样本和样本之间的关联。</p>
</blockquote>
<p>$$RNN模型：h_t = W_{xh}\cdot x_t + W_{hh}\cdot h_{t-1}$$</p>
<p>$$LSTM模型：\tilde{C}<em>t = tanh(W</em>{xi} \cdot x_t + W_{hi} \cdot h_{t-1} + b_i)$$</p>
<ul>
<li>卷积网络家族</li>
</ul>
<blockquote>
<p>使用卷积核扫描时间点/样本点，将上下文信息通过卷积计算整合到一起，以此构建样本和样本之间的关联。如下图所示，蓝绿色方框中携带权重$w$，权重与样本值对应位置元素相乘相加后生成标量，这是一个加权求和过程。</p>
</blockquote>
<p><img src="https://skojiangdoc.oss-cn-beijing.aliyuncs.com/2023DL/1dcnn/04.gif" alt="04" />
总结这些序列算法的经验，可以发现它们都是在用<strong>加权求和</strong>的方式来建立样本与样本之间的关联，因此在序列算法的发展过程中，核心问题已经由<strong>如何建立样本之间的关联</strong>转变成了<strong>如何合理对样本进行加权求和，即权重计算方式</strong>，在这个问题上，正如首次提出Transformer的论文名称——<strong>Attention is All You Need</strong>，目前最佳的权重注意力计算方式就是 <strong>注意力机制</strong>。</p>
]]></content>
    <author><name>Hygen</name></author>
  </entry>
  <entry>
    <title>LangGraph的State定义模式</title>
    <link href="https://www.hygen.red/posts/agent/langgraph/%E5%8E%9F%E7%90%86/003state%E7%8A%B6%E6%80%81%E6%A8%A1%E5%BC%8F/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/agent/langgraph/%E5%8E%9F%E7%90%86/003state%E7%8A%B6%E6%80%81%E6%A8%A1%E5%BC%8F/</id>
    <published>2025-11-16T00:00:00.000Z</published>
    <updated>2025-11-16T00:00:00.000Z</updated>
    <summary>简要介绍LangGraph框架中State状态模式的核心概念、实现机制和应用方法</summary>
    <content type="html"><![CDATA[<h1>引言</h1>
<p>LangGraph的底层图算法采用<strong>消息传递机制</strong>来定义和执行图中的交互流程，其中<code>State</code>组件扮演着关键的载体角色，负责在图的各个节点间传递信息。</p>
<h1>1. LangGraph中State的定义模式</h1>
<h3>消息流转过程</h3>
<p><img src="./b446cfde-54f0-4d6c-b239-160b53680377.png" alt="消息流转过程" />
简单过一下图中的流程：</p>
<pre><code>初始状态: { "x": "10" }
    ↓
Node 1 处理: 对 x 进行操作
    ↓
中间状态: { "x": "10" }, { "x": "11" }
    ↓
Node 2 处理: 基于 x 创建 y
    ↓
最终状态: { "x": "10" }, { "x": "11" }, { "y": "9" }
</code></pre>
<p>关键特性如下：</p>
<ol>
<li>每个节点都具备<strong>可访问</strong>、<strong>读取</strong>、<strong>写入</strong>权限</li>
<li>当某个节点修改状态时，它会将此信息广播到图中的所有节点</li>
<li><strong>State实质为一个共享的数据结构</strong>，表现为一个简单的字典</li>
<li>通过对这个字典进行读写操作，可以实现自左而右的数据流动</li>
</ol>
<p><img src="./246f21dd-2fe0-4779-af7a-fd8d3ecc1ccc.png" alt="状态设计流程" /></p>
<h2>1.1. 使用字典类型定义状态</h2>
<p>接下来用代码复现上图的状态设计流程</p>
<h3>1.1.1. 基础框架</h3>
<p>将图的状态设计为一个字典，用于在不同的节点之间<strong>共享</strong>和<strong>修改</strong>数据，然后用<code>StateGraph</code>实例化</p>
<pre><code>from langgraph.graph import StateGraph

# 创建图，使用字典作为状态模式
builder = StateGraph(dict)
</code></pre>
<h3>1.1.2. 定义节点函数</h3>
<p>这里需要定义两个节点</p>
<ol>
<li><code>addition</code>：加法逻辑，接收当前状态并将字典中的值+1</li>
<li><code>subtraction</code>：减法逻辑，接收当前状态并将字典中的值-2</li>
</ol>
<pre><code># 定义节点
def addition(state):
  print(state)
  return {"x": state["x"] + 1}

def subtraction(state):
  print(state)
  return {"x": state["x"] - 2}
</code></pre>
<h3>1.1.3. 构建图结构</h3>
<p>接下来添加节点并设置边</p>
<pre><code>from langgraph.graph import START, END

# 添加节点
builder.add_node("addition", addition)
builder.add_node("subtraction", subtraction)

# 添加边
builder.add_edge(START, "addition")
builder.add_edge("addition", "subtraction")
builder.add_edge("subtraction", END)
</code></pre>
<h3>1.1.4. 编译和执行图</h3>
<pre><code># 编译图
graph = builder.compile()

# 定义初始状态
initial_state = {"x": 10}

# 执行图
result = graph.invoke(initial_state)
print(result)
</code></pre>
<h3>1.1.5. 执行结果</h3>
<pre><code>{'x': 10}
{'x': 11}
{'x': 9}
</code></pre>
<h3>1.1.6. 关键特性</h3>
<ol>
<li>
<p><strong>节点函数只需要返回更新的部分</strong>：每个节点的函数内部逻辑中，需要使用和更新哪些State中的参数，只需要在<code>return</code>的时候指定即可，不必担心未在当前节点处理的State中的其他值会丢失。</p>
</li>
<li>
<p><strong>状态隔离性</strong>：状态在任何给定时间只包含来自一个节点的更新信息。这意味着当节点处理状态时，它只能访问与其特定操作直接相关的数据。</p>
</li>
<li>
<p><strong>字典类型的灵活性</strong>：使用字典作为状态模式非常简单，由于缺乏预定义的模式，节点可以在没有严格类型约束的情况下自由地读取和写入状态。</p>
</li>
<li>
<p><strong>注意事项</strong>：如果在任何节点中尝试访问State中不存在的键，会直接中断整个图的运行状态。
<img src="./493bf9ad-0924-473f-bb24-1b26f806020c.png" alt="Run Graph" /></p>
</li>
</ol>
<h1>2. Reducer函数机制</h1>
<h2>Reducer函数原理</h2>
<p>LangGraph的内部原理是：<code>State</code>中每一个<code>key</code>都有自己独立的<code>Reducer</code>函数，通过指定的<code>reducer</code>函数应用于状态值的更新</p>
<p><code>Reducer</code>函数用来根据当前的状态（state）和一个操作（action）来计算并返回新的状态。它是一种设计模式，用于将业务逻辑与状态变更解耦，使得状态的变更预测性更强并且容易追踪。</p>
<p>简单来说，<code>Reducer</code>就是根据给定的输入(当前状态和操作)生成新的状态。</p>
<h2>默认行为</h2>
<p>若没有显式指定，则对该键的所有更新都执行的是<strong>覆盖</strong>操作。这也解释了为什么上面的执行结果与消息流转过程的内容不一样</p>
<p>接下来请看两段代码，这边把上面的<code>dict</code>换回<code>TypeDict</code>，<code>TypeDict</code>允许我们明确指定每个键的类型，这有助于防止在状态管理过程中出现类型错误。</p>
<h2>使用TypeDict定义状态</h2>
<pre><code>from langgraph.graph import StateGraph, START, END
from typing_extensions import TypedDict

class State(TypedDict):
  x: int
  y: int

builder = StateGraph(State)

def addition(state):
  print(state)
  return {"x": state["x"] + 1}

def subtraction(state):
  print(state)
  return {"y": state["x"] - 2}

builder.add_node("addition", addition)
builder.add_node("subtraction", subtraction)

builder.add_edge(START, "addition")
builder.add_edge("addition", "subtraction")
builder.add_edge("subtraction", END)

graph = builder.compile()

initial_state = {"x": 10}
result = graph.invoke(initial_state)
print(result)
</code></pre>
<p>执行结果：</p>
<pre><code>{'x': 10}
{'x': 11}
{'x': 11, 'y': 9}
</code></pre>
<h2>使用Annotated指定Reducer</h2>
<p>当我们需要对状态进行更复杂的操作时，可以使用<code>Annotated</code>来指定<code>Reducer</code>函数：</p>
<pre><code>from langgraph.graph import StateGraph, START, END
from typing_extensions import TypedDict, Annotated, List
import operator

class State(TypedDict):
  # 消息列表，使用operator.add作为reducer函数， 意为在列表后面添加元素
  messages: Annotated[List[str], operator.add]

def addition(state):
  print(state)
  msg = state['messages'][-1] # 取最新的状态
  response = {"x": msg["x"] + 1}
  return {"messages": [response]}

def subtraction(state):
  print(state)
  msg = state['messages'][-1] # 取最新的状态
  response = {"x": msg["x"] - 2}
  return {"messages": [response]}

builder = StateGraph(State)

builder.add_node("addition", addition)
builder.add_node("subtraction", subtraction)

builder.add_edge(START, "addition")
builder.add_edge("addition", "subtraction")
builder.add_edge("subtraction", END)

graph = builder.compile()

initial_state = {"messages": [{"x": 10}]}
result = graph.invoke(initial_state)
print(result)
</code></pre>
<p>执行结果：</p>
<pre><code>{'messages': [{'x': 10}]}
{'messages': [{'x': 10}, {'x': 11}]}
{'messages': [{'x': 10}, {'x': 11}, {'x': 9}]}
</code></pre>
<p><strong>说明</strong>：<code>Annotated</code>是Python的一个类型提示工具，属于typing模块。它被用来添加额外的信息或元数据到类型提示上。</p>
<p>当定义状态模式的结构发生了变化以后，在节点函数中的读取和存储逻辑也要发生相应的变化：
<img src="./a99af143-2ef5-4b29-9119-d04fbf16048f.png" alt="Create and Run Graph" />
<strong>Reducers的目的</strong>：在LangGraph框架中的状态管理系统中，允许更灵活地定义状态如何根据各种操作更新。通过指定不同的reducer函数，我们可以控制状态的每个部分应如何响应特定的更新。</p>
<h1>3. 在图状态中处理消息的思路</h1>
<h2>构建历史对话记录</h2>
<p>Reducer机制的一个现实意义是：我们可以基于这种方式去构建历史对话记录。因为目前大多数大模型应用都是接受消息列表作为输入。就像LangChain中的Chat Model，需要接收Message对象列表作为输入。</p>
<h2>完整示例</h2>
<p>下面这个示例，我们进一步将大模型接入到LangGraph工作流程中，并允许动态消息处理以及与模型的交互：</p>
<pre><code>import os
from langchain_core.messages import SystemMessage, HumanMessage
from typing_extensions import TypedDict, Annotated, List
import operator
from langgraph.graph import StateGraph
from langchain_openai import ChatOpenAI

os.environ["OPENAI_API_KEY"] = "sk-xxx" # 填入自己的api_key

llm = ChatOpenAI(
  base_url="https://api.deepseek.com/v1",
  model='deepseek-chat',
  temperature=0
)

class State(TypedDict):
  messages: Annotated[List[str], operator.add]

builder = StateGraph(State)

def chat_with_model(state):
  print(state)
  print("-----------------")
  messages = state['messages']
  response = llm.invoke(messages)
  return {"messages": [response]}

def convert_messages(state):
  EXTRACTION_PROMPT = """
  You are a data extraction specialist tasked with retrieving key information from a text.
  Extract such information for the provided text and output it in JSON format. Outline the key data points extracted.
  """
  print(state)
  print("-----------------")
  messages = [
    SystemMessage(content=EXTRACTION_PROMPT),
    HumanMessage(content=state['messages'][-1].content)
  ]
  response = llm.invoke(messages)
  return {"messages": [response]}

builder.add_node("chat_with_model", chat_with_model)
builder.add_node("convert_messages", convert_messages)

builder.set_entry_point("chat_with_model")
builder.add_edge("chat_with_model", "convert_messages")
builder.set_finish_point("convert_messages")

graph = builder.compile()

query = "你好，请你详细的介绍一下你自己"
result = graph.invoke({"messages": [HumanMessage(content=query)]})
print(result)
</code></pre>
<p>运行结果：</p>
<pre><code>{'messages': [HumanMessage(content='你好，请你详细的介绍一下你自己', additional_kwargs={}, response_metadata={})]}
-----------------
{'messages': [HumanMessage(content='你好，请你详细的介绍一下你自己', additional_kwargs={}, response_metadata={}), AIMessage(content='你好！很高兴认识你！😊\n\n让我详细介绍一下自己：\n\n## 基本信息\n我是**DeepSeek**，由深度求索公司创造的AI助手。我是目前DeepSeek的最新版本模型，专
门设计来帮助用户解决各种问题和需求。\n\n## 核心能力\n- **文本处理**：我专注于文本的生成、理解和分析，可以帮你写作、翻译、总结、分析等\n- **知识问答**：我的知识截止到2024年7月，涵盖广泛领域，可以回答各种问题\n- **逻辑推理**：我擅长逻辑分析、数学计算、编程等问题\n- **创意写作**： 可以帮助创作故事、诗歌、文案等\n\n## 特色功能\n- **文件上传**：支持上传图像、txt、pdf、ppt、word、excel等文件，我可以读取其中的文字信息进行处理\n- **长文本处理**：拥有128K的上下文长度，可以处理很长的对话和文档\n- **联网搜索**：支持联网搜索功能（需要你在Web/App中手动开启）\n\n## 使用方式\n- **完全免费**：目前没有任何收费计划，你可以放心使用\n- **多平台**：可以通过官方应用商店下载App，或在网页端使用\n- **对话风格**：我力求回应热情、细腻，希望给你带来愉快的交流体验\n\n## 我的局限\n- 我是纯文本模型，不支持多模态识别（但可以读取上传文件中的文字）\n- 没有语 音功能\n- 知识更新到2024年7月\n\n有什么具体的问题或任务需要我帮助吗？我很乐意为你提供详细的协助！✨', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 336, 'prompt_tokens': 10, 'total_tokens': 346, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 0}, 'prompt_cache_hit_tokens': 0, 'prompt_cache_miss_tokens': 10}, 'model_name': 'deepseek-chat', 'system_fingerprint': 'fp_ffc7281d48_prod0820_fp8_kvcache', 'id': '1a853218-8f68-4e85-b4f5-d5dfb6a2341c', 'service_tier': None, 'finish_reason': 'stop', 'logprobs': None}, id='run--b712c9cc-90f6-4dbd-87a1-61ada1edc8d4-0', usage_metadata={'input_tokens': 10, 'output_tokens': 336, 'total_tokens': 346, 'input_token_details': {'cache_read': 0}, 'output_token_details': {}})]}
-----------------
{'messages': [HumanMessage(content='你好，请你详细的介绍一下你自己', additional_kwargs={}, response_metadata={}), AIMessage(content='你好！很高兴认识你！😊\n\n让我详细介绍一下自己：\n\n## 基本信息\n我是**DeepSeek**，由深度求索公司创造的AI助手。我是目前DeepSeek的最新版本模型，专 门设计来帮助用户解决各种问题和需求。\n\n## 核心能力\n- **文本处理**：我专注于文本的生成、理解和分析，可以帮你写作、翻译、总结、分析等\n- **知识问答**：我的知识截止到2024年7月，涵盖广泛领域，可以回答各种问题\n- **逻辑推理**：我擅长逻辑分析、数学计算、编程等问题\n- **创意写作**： 可以帮助创作故事、诗歌、文案等\n\n## 特色功能\n- **文件上传**：支持上传图像、txt、pdf、ppt、word、excel等文件，我可以读取其中的文字信息进行处理\n- **长文本处理**：拥有128K的上下文长度，可以处理很长的对话和文档\n- **联网搜索**：支持联网搜索功能（需要你在Web/App中手动开启）\n\n## 使用方式\n- **完全免费**：目前没有任何收费计划，你可以放心使用\n- **多平台**：可以通过官方应用商店下载App，或在网页端使用\n- **对话风格**：我力求回应热情、细腻，希望给你带来愉快的交流体验\n\n## 我的局限\n- 我是纯文本模型，不支持多模态识别（但可以读取上传文件中的文字）\n- 没有语 音功能\n- 知识更新到2024年7月\n\n有什么具体的问题或任务需要我帮助吗？我很乐意为你提供详细的协助！✨', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 336, 'prompt_tokens': 10, 'total_tokens': 346, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 0}, 'prompt_cache_hit_tokens': 0, 'prompt_cache_miss_tokens': 10}, 'model_name': 'deepseek-chat', 'system_fingerprint': 'fp_ffc7281d48_prod0820_fp8_kvcache', 'id': '1a853218-8f68-4e85-b4f5-d5dfb6a2341c', 'service_tier': None, 'finish_reason': 'stop', 'logprobs': None}, id='run--b712c9cc-90f6-4dbd-87a1-61ada1edc8d4-0', usage_metadata={'input_tokens': 10, 'output_tokens': 336, 'total_tokens': 346, 'input_token_details': {'cache_read': 0}, 'output_token_details': {}}), AIMessage(content='{\n  "name": "DeepSeek",\n  "creator": "深度求索公司",\n  "model_type": "AI助手",\n  "version_status": "最新版本",\n  "core_capabilities": [\n    "文本处理（生成、理解、分析）",\n    "知识问答",\n    "逻辑推理",\n    "创意写作"\n  ],\n  "special_features": [\n    "文件上传（图像、txt、pdf、ppt、word、excel）",\n    "长文本处理（128K上下文长度）",\n    "联网搜索（需手动开启）"\n  ],\n  "usage_details": {\n    "cost": "完全免费",\n    "platforms": ["官方应用商店App", "网页端"],\n    "interaction_style": "热情、细腻"\n  },\n  "limitations": [\n    "纯文本模型 ，不支持多模态识别",\n    "无语音功能",\n    "知识截止日期：2024年7月"\n  ]\n}\n\n**提取的关键数据点**:\n- 名称和创建者\n- 模型类型和版本状态\n- 核心能力分类\n- 特色功能列表\n- 使用方式详情（成本、平台、交互风格）\n- 已知局限性', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 256, 'prompt_tokens': 380, 'total_tokens': 636, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 0}, 'prompt_cache_hit_tokens': 0, 'prompt_cache_miss_tokens': 380}, 'model_name': 'deepseek-chat', 'system_fingerprint': 'fp_ffc7281d48_prod0820_fp8_kvcache', 'id': '468b4734-91c2-4b7c-bb44-c81f83c97db2', 'service_tier': None, 'finish_reason': 'stop', 'logprobs': None}, id='run--7aaaca20-87e9-455f-8e2c-88b2766062d9-0', usage_metadata={'input_tokens': 380, 'output_tokens': 256, 'total_tokens': 636, 'input_token_details': {'cache_read': 0}, 'output_token_details': {}})]}
</code></pre>
<p>这个内容可以自己复制到python文件中print一下，再复制到markdown文件中渲染一下</p>
<p>这里需要注意的是，<code>SystemMessage</code>的内容是不会返回的，所以不要误以为是bug。</p>
<h1>4. MessageGraph</h1>
<p><code>MessageGraph</code>是<code>StateGraph</code>的一个子类，使用了<code>Annotated[list[AnyMessage], add_messages]</code>来初始化其基类StateGraph。</p>
<h2>源码定义</h2>
<pre><code>class MessageGraph(StateGraph):
  def __init__(self, state_schema) -&gt; None:
    super().__init__(Annotated[List[list[AnyMessage]], add_messages])
</code></pre>
<ul>
<li><code>MessageGraph</code>中的每个节点都将消息列表作为输入，并返回零个或多个消息作为输出</li>
<li><code>add_messages</code>函数用于将每个节点的输出消息合并进图的状态中已存在的消息列表</li>
<li><code>MessageGraph</code>通过使用单个仅附加消息列表作为其整个状态来管理状态，特别适合对话式应用程序</li>
</ul>
<h2>add_messages函数</h2>
<p><strong>核心逻辑</strong>： <code>add_messages</code>整体的核心逻辑是合并两个消息列表，按<code>ID</code>更新现有消息。默认情况下，状态为"仅附加"，当新消息与现有消息具有相同的ID时，进行更新。</p>
<p>参数说明：</p>
<ul>
<li><code>left</code>(Messages) - 消息的基本列表</li>
<li><code>right</code>(Messages) - 要合并到基本列表中的消息列表(或单个消息)</li>
</ul>
<p><strong>返回值</strong>：一个消息列表，其中的合并逻辑是：如果right的消息与left的消息具有相同的ID，则right的消息将替换left的消息，否则作为一条新的消息进行追加。</p>
<h3>示例</h3>
<pre><code>from langchain_core.messages import HumanMessage, AIMessage
from langgraph.graph.message import add_messages

msgs1 = [HumanMessage(content="你好。", id="1")]
msgs2 = [AIMessage(content="你好，很高兴认识你。", id="2")]
print(add_messages(msgs1, msgs2))

msgs1 = [HumanMessage(content="你好。", id="1")]
msgs2 = [HumanMessage(content="你好，很高兴认识你。", id="1")]
print(add_messages(msgs1, msgs2))
</code></pre>
<p>运行结果：</p>
<pre><code>[HumanMessage(content='你好。', additional_kwargs={}, response_metadata={}, id='1'), AIMessage(content='你好，很高兴认识你。', additional_kwargs={}, response_metadata={}, id='2')]
[HumanMessage(content='你好，很高兴认识你。', additional_kwargs={}, response_metadata={}, id='1')]
</code></pre>
<p>从这里显然可以看出id相同会覆盖，id不同则会连接</p>
<h2>StateGraph vs MessageGraph</h2>
<ul>
<li><code>MessageGraph</code>：专门用于以消息为中心的工作流程</li>
<li><code>StateGraph</code>：更通用，适用于更广泛的应用程序，允许更复杂的状态结构，其中状态可以是任何Python类型（如TypedDict或Pydantic模型）</li>
</ul>
<h3>在StateGraph中使用add_messages</h3>
<pre><code>import os
from typing_extensions import Annotated
from langchain_openai import ChatOpenAI
from langgraph.graph.message import add_messages
from typing_extensions import TypedDict
from langgraph.graph import StateGraph
from langchain_core.messages import HumanMessage

os.environ["OPENAI_API_KEY"] = "sk-xxx" # 使用自己的api-key

class State(TypedDict):
  messages: Annotated[list, add_messages]

llm = ChatOpenAI(
  base_url="https://api.deepseek.com/v1",
  model="deepseek-chat",
  temperature=0
)

def chatbot(state):
  return {"messages": [llm.invoke(state["messages"])]}

graph_builder = StateGraph(State)

graph_builder.add_node("chatbot", chatbot)
graph_builder.set_entry_point("chatbot")
graph_builder.set_finish_point("chatbot")

graph = graph_builder.compile()

def stream_graph_updates(user_input: str):
  for event in graph.stream({"messages": [HumanMessage(content=user_input)]}):
    for value in event.values():
      print("模型回复:", value["messages"][-1].content)

while True:
  try:
    user_input = input("用户提问: ")
    if user_input.lower() in ["exit"]:
      print("下次再见！")
      break
    stream_graph_updates(user_input)
  except:
    break
</code></pre>
<p>运行结果：</p>
<pre><code>用户提问: 请用一句话介绍你自己
模型回复: 你好，我是DeepSeek，一个由深度求索公司创造的AI助手，热爱学习新知识，也乐于用我的能力为你提供各种帮助！😊

有什么我可以帮你的吗？
用户提问: exit
下次再见！
</code></pre>
]]></content>
    <author><name>Hygen</name></author>
  </entry>
  <entry>
    <title>LangGraph接入大模型做问答流程</title>
    <link href="https://www.hygen.red/posts/agent/langgraph/%E5%BA%94%E7%94%A8/001-langgraph%E5%A6%82%E4%BD%95%E6%8E%A5%E5%85%A5%E5%A4%A7%E6%A8%A1%E5%9E%8B%E5%81%9A%E9%97%AE%E7%AD%94%E6%B5%81%E7%A8%8B/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/agent/langgraph/%E5%BA%94%E7%94%A8/001-langgraph%E5%A6%82%E4%BD%95%E6%8E%A5%E5%85%A5%E5%A4%A7%E6%A8%A1%E5%9E%8B%E5%81%9A%E9%97%AE%E7%AD%94%E6%B5%81%E7%A8%8B/</id>
    <published>2025-11-13T00:00:00.000Z</published>
    <updated>2025-11-13T00:00:00.000Z</updated>
    <summary>学习使用LangGraph做大模型问答</summary>
    <content type="html"><![CDATA[<h1>1. 概述</h1>
<p>本博客内容主要介绍如何将大模型集成到<code>LangGraph</code>框架中，构建一个有应用价值的问答流程。通过学习，我们可以：</p>
<ul>
<li>了解如何在<code>LangGraph</code>中集成大语言模型</li>
<li>掌握构建问答流程的完整步骤</li>
<li>学习如何扩展图结构以实现更复杂的功能</li>
</ul>
<h1>2. 大模型接入方式</h1>
<h2>2.1. 模型支持</h2>
<p><code>LangGraph</code>对目前主流的在线或开源模型均支持接入，包括：</p>
<ul>
<li><strong>在线模型</strong>：OpenAI GPT系列、Anthropic Claude等</li>
<li><strong>开源模型</strong>：GLM4、LLaMA3、Qwen等</li>
</ul>
<h2>2.2. 接入方式</h2>
<p>可以通过以下方式将大模型集成到<code>LangGraph</code>中：</p>
<ol>
<li><strong>使用LangChain框架</strong>：通过<code>LangChain</code>提供的接口（如<code>ChatOpenAI</code>）接入</li>
<li><strong>原生API方式</strong>：直接使用大模型的原生API接口</li>
<li><strong>推理加速库</strong>：利用<code>Ollama</code>、<code>vLLM</code>等推理加速库实现高效集成</li>
</ol>
<blockquote>
<p><strong>参考文档</strong>：</p>
<ul>
<li>LangChain Chat集成：https://python.langchain.com/docs/integrations/chat/</li>
<li>LangChain LLM集成：https://python.langchain.com/docs/integrations/llms/</li>
</ul>
</blockquote>
<h1>3. 基础问答流程构建</h1>
<h2>3.1. 定义图模式</h2>
<p>首先需要定义图的状态模式，包括输入状态、输出状态和整体状态：</p>
<pre><code>from langgraph.graph import StateGraph
from typing_extensions import TypedDict
from langgraph.graph import START, END

# 定义输入的模式
class InputState(TypedDict):
    question: str

# 定义输出的模式
class OutputState(TypedDict):
    answer: str

# 将 InputState 和 OutputState 这两个 TypedDict 类型合并成一个更全面的字典类型
class OverallState(InputState, OutputState):
    pass
</code></pre>
<p><strong>关键点说明</strong>：</p>
<ul>
<li><code>InputState</code>：定义用户输入的格式，包含<code>question</code>字段</li>
<li><code>OutputState</code>：定义系统输出的格式，包含<code>answer</code>字段</li>
<li><code>OverallState</code>：通过多重继承合并输入和输出状态，形成完整的状态定义</li>
</ul>
<h2>3.2. 创建LLM节点</h2>
<p>创建一个节点函数，用于接收用户问题并调用大模型生成回答：</p>
<pre><code>from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
import getpass
import os

# 设置API密钥
if not os.environ.get("OPENAI_API_KEY"):
    os.environ["OPENAI_API_KEY"] = getpass.getpass("Enter your OpenAI API key: ")

def llm_node(state: InputState):
    messages = [
        ("system", "你是一位乐于助人的智能小助理"),
        ("human", state["question"])
    ]
    
    llm = ChatOpenAI(model="gpt-4o", temperature=0)
    response = llm.invoke(messages)
    
    return {"answer": response.content}
</code></pre>
<p><strong>节点函数特点</strong>：</p>
<ol>
<li><strong>输入参数</strong>：接收<code>state: InputState</code>作为第一个位置参数</li>
<li><strong>消息构建</strong>：使用系统提示词和用户问题构建消息列表</li>
<li><strong>模型调用</strong>：使用<code>ChatOpenAI</code>调用GPT-4o模型</li>
<li><strong>返回值</strong>：返回包含<code>answer</code>字段的字典，更新图状态</li>
</ol>
<h2>3.3. 构建图结构</h2>
<p>将节点添加到图中，并建立节点间的连接关系：</p>
<pre><code># 明确指定它的输入和输出数据的结构或模式
builder = StateGraph(OverallState, input=InputState, output=OutputState)

# 添加节点
builder.add_node("llm_node", llm_node)

# 添加边
builder.add_edge(START, "llm_node")
builder.add_edge("llm_node", END)

# 编译图
graph = builder.compile()
</code></pre>
<p><strong>图结构说明</strong>：</p>
<ul>
<li><code>StateGraph</code>初始化时指定了状态模式、输入模式和输出模式</li>
<li><code>add_node</code>方法添加节点，第一个参数是节点名称，第二个参数是节点函数</li>
<li><code>add_edge</code>方法添加边，连接<code>START</code> → <code>llm_node</code> → <code>END</code></li>
<li><code>compile</code>方法编译图，验证图结构的合法性</li>
</ul>
<h2>3.4. 执行测试</h2>
<p>使用<code>invoke</code>方法调用图，传入用户问题：</p>
<pre><code># 基础测试
result = graph.invoke({"question": "你好，我用来测试"})
print(result["answer"])

# 详细问题测试
result = graph.invoke({"question": "你好，请你详细的介绍一下你自己"})
print(result["answer"])
</code></pre>
<p><strong>执行流程</strong>：</p>
<ol>
<li>用户输入通过<code>START</code>节点进入图</li>
<li><code>llm_node</code>节点接收问题，调用大模型生成回答</li>
<li>结果通过<code>END</code>节点返回给用户</li>
</ol>
<h1>4. 扩展：多节点问答流程</h1>
<h2>4.1. 扩展状态定义</h2>
<p>为了支持多节点间的数据传递，需要扩展状态定义：</p>
<pre><code>from typing_extensions import TypedDict, Optional

# 定义输入的模式
class InputState(TypedDict):
    question: str
    llm_answer: Optional[str]  # 表示 llm_answer 可以是 str 类型，也可以是 None

# 定义输出的模式
class OutputState(TypedDict):
    answer: str

# 将 InputState 和 OutputState 这两个 TypedDict 类型合并成一个更全面的字典类型
class OverallState(InputState, OutputState):
    pass
</code></pre>
<p><strong>关键变化</strong>：</p>
<ul>
<li>添加了<code>llm_answer</code>字段，用于存储中间节点的输出结果</li>
<li>使用<code>Optional[str]</code>表示该字段可以为<code>None</code>，提供灵活性</li>
</ul>
<h2>4.2. 创建多个节点</h2>
<p>定义两个节点：一个用于生成回答，另一个用于翻译：</p>
<pre><code>def llm_node(state: InputState):
    messages = [
        ("system", "你是一位乐于助人的智能小助理"),
        ("human", state["question"])
    ]
    
    llm = ChatOpenAI(model="gpt-4o", temperature=0)
    response = llm.invoke(messages)
    
    return {"llm_answer": response.content}

def action_node(state: InputState):
    messages = [
        ("system", "无论你接收到什么语言的文本，请翻译成法语"),
        ("human", state["llm_answer"])
    ]
    
    llm = ChatOpenAI(model="gpt-4o", temperature=0)
    response = llm.invoke(messages)
    
    return {"answer": response.content}
</code></pre>
<p><strong>节点功能说明</strong>：</p>
<ul>
<li><code>llm_node</code>：接收用户问题，生成中文回答，并将结果存储在<code>llm_answer</code>字段</li>
<li><code>action_node</code>：接收<code>llm_answer</code>，将其翻译成法语，并将结果存储在<code>answer</code>字段</li>
</ul>
<h2>4.3. 构建多节点图结构</h2>
<p>将两个节点添加到图中，并建立连接：</p>
<pre><code># 明确指定它的输入和输出数据的结构或模式
builder = StateGraph(OverallState, input=InputState, output=OutputState)

# 添加节点
builder.add_node("llm_node", llm_node)
builder.add_node("action_node", action_node)

# 添加边
builder.add_edge(START, "llm_node")
builder.add_edge("llm_node", "action_node")
builder.add_edge("action_node", END)

# 编译图
graph = builder.compile()
</code></pre>
<p><strong>图结构流程</strong>：</p>
<pre><code>START → llm_node → action_node → END
</code></pre>
<p><strong>执行流程</strong>：</p>
<ol>
<li>用户问题通过<code>START</code>进入<code>llm_node</code></li>
<li><code>llm_node</code>生成中文回答，更新<code>llm_answer</code>字段</li>
<li><code>action_node</code>读取<code>llm_answer</code>，翻译成法语，更新<code>answer</code>字段</li>
<li>最终结果通过<code>END</code>返回</li>
</ol>
<h2>4.4. 测试多节点流程</h2>
<pre><code># 测试自我介绍翻译
result = graph.invoke({"question": "你好，请你详细的介绍一下你自己"})
print(result["answer"])

# 测试知识问答翻译
result = graph.invoke({"question": "请问什么是人工智能？"})
print(result["answer"])
</code></pre>
<h1>5. 实测代码(基于Deepseek)</h1>
<h2>5.1. 单Agent流程</h2>
<pre><code>from langgraph.graph import StateGraph
from typing_extensions import TypedDict
from langgraph.graph import START, END

# 1. 创建图模式
# 为什么参数是TypedDict？
# TypedDict是Python中的一个类型，用于定义字典的类型。
# 它可以帮助我们定义字典的键和值的类型，从而避免在运行时出现类型错误。
class InputState(TypedDict):
  question: str

class OutputState(TypedDict):
  answer: str

# pass 和 ... 的区别
# pass 是一个占位符，表示什么也不做。
# ... 表示一个空字典。
class OverallState(InputState, OutputState):
  pass

# 2. 创建节点函数
# 获取api key
import os
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.messages import SystemMessage, HumanMessage

# 设置api key，这边直接配置了key，要科学保密可以使用dotenv来进行配置
os.environ["OPENAI_API_KEY"] = "sk-xxx" # 填自己的api-key


def llm_node(state: OverallState) -&gt; OverallState:
  messages = [
    SystemMessage(content="你是一位乐于助人的智能小助理"),
    HumanMessage(content=state["question"])
  ]
  llm = ChatOpenAI(
    model="deepseek-chat", 
    temperature=0,
    base_url="https://api.deepseek.com/v1"
  )
  response = llm.invoke(messages)
  return {"answer": response.content}

# 3. 创建图结构
builder = StateGraph(OverallState, input_schema=InputState, output_schema=OutputState)
builder.add_node("llm_node", llm_node)
builder.add_edge(START, "llm_node")
builder.add_edge("llm_node", END)

# 4. 编译图
graph = builder.compile()

# 5. 执行图
result = graph.invoke({"question": "你好，我用来测试"})
print(result["answer"])
</code></pre>
<p>运行截图：
<img src="./3be9d465-dd13-47c3-acb6-f498ab7690f3.png" alt="运行截图" /></p>
<h2>5.2. 多Agent流程</h2>
<pre><code>import os
from typing import Optional
from typing_extensions import TypedDict
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage
from langgraph.graph import StateGraph, START, END

class InputState(TypedDict):
  question: str
  llm_answer: Optional[str]

class OutputState(TypedDict):
  answer: str

class OverallState(InputState, OutputState):
  pass

os.environ["OPENAI_API_KEY"] = "sk-xxx" # 修改为自己的api-key

def llm_node(state: OverallState) -&gt; OverallState:
  messages = [
    SystemMessage(content="你是一位乐于助人的智能小助理"),
    HumanMessage(content=state["question"])
  ]
  llm = ChatOpenAI(
    model="deepseek-chat", 
    temperature=0,
    base_url="https://api.deepseek.com/v1"
  )
  response = llm.invoke(messages)
  return {"llm_answer": response.content}

def action_node(state: OverallState) -&gt; OverallState:
  messages = [
    SystemMessage(content="请将你接收到的文本翻译为用英文"),
    HumanMessage(content=state["llm_answer"])
  ]
  llm = ChatOpenAI(
    model="deepseek-chat", 
    temperature=0,
    base_url="https://api.deepseek.com/v1"
  )
  response = llm.invoke(messages)
  return {"answer": response.content}

builder = StateGraph(OverallState, input_schema=InputState, output_schema=OutputState)
builder.add_node("llm_node", llm_node)
builder.add_node("action_node", action_node)
builder.add_edge(START, "llm_node")
builder.add_edge("llm_node", "action_node")
builder.add_edge("action_node", END)
graph = builder.compile()
result = graph.invoke({"question": "你好，请你详细的介绍一下你自己"})
print(result["answer"])
</code></pre>
<p>运行截图：
<img src="./d3bfef62-16e0-428e-aa0a-324b1da935fb.png" alt="多Agent" /></p>
<h1>6. 核心要点总结</h1>
<h2>6.1. LangGraph的灵活性</h2>
<ol>
<li>
<p><strong>节点函数自主性</strong>：</p>
<ul>
<li>可以在节点函数中定义任意逻辑</li>
<li>可以自主构建中间状态信息</li>
<li>不强制依赖<code>LangChain</code>框架</li>
</ul>
</li>
<li>
<p><strong>状态管理</strong>：</p>
<ul>
<li>节点可以读取和写入图状态中的任何字段</li>
<li>状态在节点间自动传递和更新</li>
<li>通过<code>TypedDict</code>确保类型安全</li>
</ul>
</li>
<li>
<p><strong>图结构扩展性</strong>：</p>
<ul>
<li>可以轻松添加新节点</li>
<li>通过边灵活控制执行流程</li>
<li>支持复杂的多节点工作流</li>
</ul>
</li>
</ol>
<h2>6.2. 与LangChain的关系</h2>
<ul>
<li><strong>基于LangChain</strong>：<code>LangGraph</code>基于<code>LangChain</code>的表达式语言构建</li>
<li><strong>可独立运行</strong>：虽然基于<code>LangChain</code>，但完全可以脱离<code>LangChain</code>独立运行</li>
<li><strong>模型接入灵活</strong>：可以使用<code>LangChain</code>接口，也可以使用原生API</li>
</ul>
<h2>6.3. 实践建议</h2>
<ol>
<li><strong>理解状态模式</strong>：深入理解<code>TypedDict</code>的作用和状态传递机制</li>
<li><strong>节点函数设计</strong>：合理设计节点函数，确保输入输出格式正确</li>
<li><strong>图结构规划</strong>：在构建图之前，先规划好节点和边的连接关系</li>
<li><strong>逐步扩展</strong>：从简单流程开始，逐步添加复杂功能</li>
</ol>
<h1>7. 学习要点</h1>
<p>通过本节的学习，需要掌握：</p>
<ol>
<li>✅ 如何在<code>LangGraph</code>中定义图的状态模式</li>
<li>✅ 如何创建节点函数并集成大语言模型</li>
<li>✅ 如何构建图结构并建立节点间的连接</li>
<li>✅ 如何执行图并获取结果</li>
<li>✅ 如何扩展图结构以实现多节点工作流</li>
<li>✅ 理解节点间状态传递的机制</li>
</ol>
<h1>8. 下一步学习方向</h1>
<p>掌握了基础的问答流程构建后，可以进一步学习：</p>
<ul>
<li><strong>条件边（Conditional Edges）</strong>：根据条件动态选择执行路径</li>
<li><strong>循环图（Cyclic Graphs）</strong>：实现循环和迭代逻辑</li>
<li><strong>状态持久化</strong>：保存和恢复图执行状态</li>
<li><strong>人机交互</strong>：在图中插入人工审核节点</li>
<li><strong>流式输出</strong>：实现实时流式响应</li>
</ul>
<hr />
<p><strong>注意</strong>：本节示例虽然不复杂，但涉及的知识点和细节较多。建议亲自尝试和体验，打好扎实的基础，才能更好地开展后续复杂循环图的学习。</p>
]]></content>
    <author><name>Hygen</name></author>
  </entry>
  <entry>
    <title>字符串解码</title>
    <link href="https://www.hygen.red/posts/dsa/solution/002-%E5%AD%97%E7%AC%A6%E4%B8%B2%E8%A7%A3%E7%A0%81/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/dsa/solution/002-%E5%AD%97%E7%AC%A6%E4%B8%B2%E8%A7%A3%E7%A0%81/</id>
    <published>2025-11-12T00:00:00.000Z</published>
    <updated>2025-11-12T00:00:00.000Z</updated>
    <summary>Leetcode-394</summary>
    <content type="html"><![CDATA[<h1>题目描述</h1>
<p>给定一个经过编码的字符串，返回它的解码字符串。</p>
<h2>编码规则</h2>
<p>编码规则为：<code>k[encoded_string]</code>，表示方括号内部的 <code>encoded_string</code> 正好重复 <code>k</code> 次。注意 <code>k</code> 保证为正整数。</p>
<h2>假设和约束</h2>
<ul>
<li>输入字符串总是有效的。</li>
<li>输入字符串中没有额外的空格。</li>
<li>输入的方括号总是符合格式要求的（即括号是平衡的）。</li>
<li>原始数据不包含数字，所有的数字只表示重复的次数 <code>k</code>。例如，不会出现像 <code>3a</code> 或 <code>2[4]</code> 这样的输入。</li>
<li>解码后的字符串最大长度保证不超过 <code>10^5</code>。</li>
</ul>
<h2>示例</h2>
<h3>示例 1</h3>
<p><strong>输入：</strong> <code>s = "3[a]2[bc]"</code></p>
<p><strong>输出：</strong> <code>"aaabcbc"</code></p>
<p><strong>解释：</strong> <code>3[a]</code> 解码为 <code>aaa</code>。<code>2[bc]</code> 解码为 <code>bcbc</code>。将它们连接起来得到 <code>aaabcbc</code>。</p>
<h3>示例 2</h3>
<p><strong>输入：</strong> <code>s = "3[a2[c]]"</code></p>
<p><strong>输出：</strong> <code>"accaccacc"</code></p>
<p><strong>解释：</strong> 这个例子展示了嵌套编码。首先，内层括号中的 <code>2[c]</code> 解码为 <code>cc</code>。所以 <code>a2[c]</code> 变成 <code>acc</code>。然后，<code>3[acc]</code> 解码为 <code>accaccacc</code>。</p>
<h3>示例 3</h3>
<p><strong>输入：</strong> <code>s = "2[abc]3[cd]ef"</code></p>
<p><strong>输出：</strong> <code>"abcabccdcdcdef"</code></p>
<p><strong>解释：</strong> <code>2[abc]</code> 解码为 <code>abcabc</code>。<code>3[cd]</code> 解码为 <code>cdcdcd</code>。<code>ef</code> 保持不变。将这些部分连接起来得到 <code>abcabccdcdcdef</code>。</p>
<h3>示例 4</h3>
<p><strong>输入：</strong> <code>s = "abc3[cd]xyz"</code></p>
<p><strong>输出：</strong> <code>"abccdcdcdxyz"</code></p>
<p><strong>解释：</strong> <code>abc</code> 保持不变。<code>3[cd]</code> 解码为 <code>cdcdcd</code>。<code>xyz</code> 保持不变。将这些部分连接起来得到 <code>abccdcdcdxyz</code>。</p>
<h2>提示</h2>
<ul>
<li><code>1 &lt;= s.length &lt;= 30</code></li>
<li><code>s</code> 由小写英文字母、数字（<code>0-9</code>）和方括号 <code>'['</code> 和 <code>']'</code> 组成</li>
<li><code>s</code> 保证是一个有效的输入</li>
<li><code>s</code> 中所有整数的取值范围为 <code>[1, 300]</code></li>
</ul>
<h1>解题思路</h1>
<p>根据提示中的内容，可以知道我们接收到的输入只有四种情况：</p>
<ol>
<li>英文字母</li>
<li>数字</li>
<li>左方括号 <code>[</code></li>
<li>右方括号 <code>]</code></li>
</ol>
<h2>核心思想</h2>
<p>这是一个典型的<strong>栈</strong>应用问题。由于编码字符串可能包含嵌套结构（如示例 2 中的 <code>3[a2[c]]</code>），我们需要使用栈来保存外层的信息，以便在处理完内层后能够正确恢复。</p>
<h2>算法步骤</h2>
<ol>
<li>
<p><strong>使用栈存储上下文</strong>：栈中存储 <code>(重复次数, 前缀字符串)</code> 的元组，用于保存遇到 <code>[</code> 之前的状态。</p>
</li>
<li>
<p><strong>遍历字符串，根据不同字符类型进行处理</strong>：</p>
<ul>
<li><strong>数字字符</strong>：连续读取所有数字字符，转换为整数，作为后续 <code>[...]</code> 的重复次数。</li>
<li><strong>左方括号 <code>[</code></strong>：遇到 <code>[</code> 时，说明即将开始一个新的编码块。此时将当前的重复次数和已构建的字符串压入栈中保存，然后重置当前状态（重复次数设为 0，字符串清空），准备处理内层内容。</li>
<li><strong>右方括号 <code>]</code></strong>：遇到 <code>]</code> 时，说明当前编码块结束。从栈中弹出之前保存的状态 <code>(pre_num, pre_str)</code>，将当前字符串重复 <code>pre_num</code> 次，然后拼接到 <code>pre_str</code> 后面，作为新的当前字符串。</li>
<li><strong>英文字母</strong>：直接添加到当前字符串中。</li>
</ul>
</li>
<li>
<p><strong>返回结果</strong>：遍历完成后，当前字符串即为解码后的结果。</p>
</li>
</ol>
<h2>算法示例</h2>
<p>以 <code>"3[a2[c]]"</code> 为例，演示算法执行过程：</p>
<ul>
<li>遇到 <code>3</code>：<code>cur_num = 3</code></li>
<li>遇到 <code>[</code>：将 <code>(3, "")</code> 压入栈，重置 <code>cur_num = 0</code>, <code>cur_str = []</code></li>
<li>遇到 <code>a</code>：<code>cur_str = ['a']</code></li>
<li>遇到 <code>2</code>：<code>cur_num = 2</code></li>
<li>遇到 <code>[</code>：将 <code>(2, "a")</code> 压入栈，重置 <code>cur_num = 0</code>, <code>cur_str = []</code></li>
<li>遇到 <code>c</code>：<code>cur_str = ['c']</code></li>
<li>遇到 <code>]</code>：弹出 <code>(2, "a")</code>，<code>cur_str = ["a" + 2 * "c"] = ["acc"]</code></li>
<li>遇到 <code>]</code>：弹出 <code>(3, "")</code>，<code>cur_str = ["" + 3 * "acc"] = ["accaccacc"]</code></li>
<li>返回 <code>"accaccacc"</code></li>
</ul>
<h2>时间复杂度</h2>
<ul>
<li><strong>时间复杂度</strong>：$O(n)$，其中 n 是解码后字符串的长度。每个字符最多被处理一次。</li>
<li><strong>空间复杂度</strong>：$O(n)$，栈的空间复杂度取决于嵌套层数，最坏情况下为 $O(n)$。</li>
</ul>
<h1>代码实现</h1>
<pre><code>class Solution:
    def decodeString(self, s: str) -&gt; str:
        stk: List[Tuple[int, str]] = []
        cur_num = 0
        cur_str = []
        i = 0
        while i &lt; len(s):
            c = s[i]
            if c.isdigit():
                res = ''
                while s[i].isdigit():
                    res += s[i]
                    i += 1
                cur_num = int(res)
                i -= 1
            elif c == '[':
                stk.append((cur_num, ''.join(cur_str)))
                cur_num = 0
                cur_str = []
            elif c == ']':
                pre_num, pre_str = stk.pop()
                cur_str = [pre_str + pre_num * ''.join(cur_str)]
            else:
                cur_str.append(c)
            i += 1
        return ''.join(cur_str)
</code></pre>
]]></content>
    <author><name>Hygen</name></author>
    <category term="Algorithm and Structure"/>
  </entry>
  <entry>
    <title>简化attention输出的元素总和</title>
    <link href="https://www.hygen.red/posts/dsa/solution/001-%E7%AE%80%E5%8C%96attention%E8%BE%93%E5%87%BA%E7%9A%84%E5%85%83%E7%B4%A0%E6%80%BB%E5%92%8C/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/dsa/solution/001-%E7%AE%80%E5%8C%96attention%E8%BE%93%E5%87%BA%E7%9A%84%E5%85%83%E7%B4%A0%E6%80%BB%E5%92%8C/</id>
    <published>2025-11-11T00:00:00.000Z</published>
    <updated>2025-11-11T00:00:00.000Z</updated>
    <summary>华为2025秋招AI岗笔试题</summary>
    <content type="html"><![CDATA[<p>本题出自2025年秋招-华为-9.17AI岗笔试</p>
<h1>题目描述</h1>
<p>给定三个正整数 n、m、h（均小于 100），构造如下数据并计算结果。</p>
<p><strong>数据构造规则：</strong></p>
<ul>
<li>输入特征矩阵 <strong>X</strong> 为 n×m 的全 1 矩阵。</li>
<li>三个权重矩阵 <strong>W1</strong>、<strong>W2</strong>、<strong>W3</strong> 均为 m×h 的"上三角全 1"矩阵（按行列索引在主对角线及其上方位置为1，其余为0；当 m≥h 时视为按行列索引的上三角扩展）。</li>
<li>令 <strong>Q = X·W1</strong>，<strong>K = X·W2</strong>，<strong>V = X·W3</strong>；计算 <strong>S = (Q·K^T) / sqrt(h)</strong></li>
<li>softmax 按行做"归一化"：对任意行向量 r，softmax(r) 的每个元素等于该元素除以本行所有元素之和。</li>
<li><strong>Y = softmax(S)·V</strong></li>
</ul>
<p><strong>输出要求：</strong> 求矩阵 <strong>Y</strong> 所有元素的和，四舍五入到整数后输出</p>
<p><strong>时间限制：</strong> C/C++ 1秒，其他语言 2秒<br />
<strong>空间限制：</strong> C/C++ 256M，其他语言 512M</p>
<p><strong>输入描述：</strong></p>
<ul>
<li>一行，三个正整数 n m h（均小于 100，且均&gt;0）</li>
</ul>
<p><strong>输出描述：</strong></p>
<ul>
<li>一行，一个整数：矩阵 Y 的元素和（四舍五入后）</li>
</ul>
<p><strong>示例1</strong></p>
<pre><code>输入例子：5 4 3
输出例子：30
例子说明：h≤m，单行和为 1+2+3=6；总和=n×6=5×6=30
</code></pre>
<h1>解题思路</h1>
<p>本题是一个模拟题，它有数学推理后$O(1)$的解法，但是在此不多做讨论，我们主要讨论$O(nm)$的解法。，也就是模拟的做法</p>
<p>其实题目已经将要做什么告诉我们了，我们只需要按照题目的要求一步一步来即可，直接看代码实现即可</p>
<h1>代码实现</h1>
<pre><code>import numpy as np
# 输入n, m, h
n, m, h = map(int, input().split())
# 构造X
X = np.ones((n, m))
# 构造W
W = np.triu(np.ones((m, h)))
Q = X @ W
K = X @ W
V = X @ W
S = (Q @ K.T) / np.sqrt(h)
row_sums = S.sum(axis=1, keepdims=True)
softmax_S = S / row_sums
Y = softmax_S @ V
print(int(round(np.sum(Y))))
</code></pre>
<p>语法知识补充：</p>
<ul>
<li><code>np.ones((n, m))</code>：生成一个 n×m 的全 1 矩阵</li>
<li><code>np.triu(np.ones((m, h)))</code>：生成一个 m×h 的上三角全 1 矩阵</li>
<li><code>@</code>：矩阵乘法运算符</li>
<li><code>np.sqrt(h)</code>：计算 h 的平方根</li>
<li><code>np.sum(S, axis=1, keepdims=True)</code>：计算 S 按行求和，并保持维度</li>
<li><code>np.round(Y)</code>：四舍五入 Y 的每个元素</li>
</ul>
]]></content>
    <author><name>Hygen</name></author>
    <category term="Algorithm and Structure"/>
  </entry>
  <entry>
    <title>LangGraph底层源码解析</title>
    <link href="https://www.hygen.red/posts/agent/langgraph/%E5%8E%9F%E7%90%86/002%E5%BA%95%E5%B1%82%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/" rel="alternate" type="text/html"/>
    <id>https://www.hygen.red/posts/agent/langgraph/%E5%8E%9F%E7%90%86/002%E5%BA%95%E5%B1%82%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/</id>
    <published>2025-11-08T00:00:00.000Z</published>
    <updated>2025-11-08T00:00:00.000Z</updated>
    <summary>graph源码解析</summary>
    <content type="html"><![CDATA[<h1>1. LangGraph介绍</h1>
<p>官方文档：<a href="https://docs.langchain.com/oss/python/langgraph/overview">点此跳转</a></p>
<p>正如数据结构中的链表和图的关系，传统的AI工作流为链式调用，这就好比一个链表，它很直观易懂，但是局限性太强，如果想要基于某个条件选择不同的Agent进行工作，链式工作流就捉襟见肘了，如果当我在某处触发某个条件需要回溯到先前的节点，链式工作流更是无法实现。</p>
<p>而LangGraph提供了一种图结构，将各个节点以图的形式组织起来，从而实现更灵活的调用方式。</p>
<p>与图类似的，LangGraph也有节点(Node)和边(Edge)两个概念，<strong>节点</strong>不一定是Agent，也可以是任意对象，如LLM、工作流、以及可运行的如函数等，<strong>边</strong>则表示两个节点组成了一个工作流，注意边是有向边，即有明确的调用顺序。</p>
<p>与图不同的是，LangGraph还有几个新概念，<strong>Action</strong>和<strong>Router</strong>,严格来说，Action和Router都是节点，但是它们有特殊的含义，Action表示一个可运行的函数，Router表示一个选择器，它根据某些条件选择下一个节点。</p>
<p>简单来说，Action表示的是一个节点的业务逻辑，Router则是一个特殊的节点，它包含的是路由逻辑而非业务逻辑，用炒菜来举个例子，假设我们有一个炒菜的Agent，它需要调用不同的工具和食材，那么这些工具和食材就是节点，而炒菜的过程就是边，而Action就是具体的炒菜步骤，Router就是选择食材和工具的步骤。</p>
<p>在LangGraph框架中，<code>Router</code>使用<code>if...else</code>的形式来决定路径，主要通过以下三种方式实现：</p>
<ul>
<li><strong>提示词工程</strong>：指示大模型以特定格式作出回应</li>
<li><strong>输出解析器</strong>：使用后处理从大模型响应中提取结构化数据</li>
<li><strong>工具调用</strong>：利用大模型的内置工具调用功能来生成结构化输出</li>
</ul>
<h1>2. LangGraph安装</h1>
<p>在此我使用<code>uv</code>来进行python的包管理。</p>
<pre><code>uv init test
cd test
uv add langgraph
</code></pre>
<p>如果是<code>pip</code>，运行<code>pip install langgraph</code>即可</p>
<h1>3. LangGraph state.py 源码解读</h1>
<blockquote>
<p>💡 <strong>通俗理解</strong>：这个文件就像是 LangGraph 的"大脑"，它定义了如何构建和执行一个工作流图。想象一下，你有一个复杂的任务需要多个步骤完成，每个步骤（节点）都需要从共享的"工作台"（状态）上读取信息，处理完后把结果放回工作台。这个文件就是定义这个工作台和工作流程的规则。</p>
</blockquote>
<h2>目录</h2>
<ol>
<li><a href="#%E5%AF%BC%E5%85%A5%E9%83%A8%E5%88%86">导入部分</a></li>
<li><a href="#%E5%B7%A5%E5%85%B7%E5%87%BD%E6%95%B0">工具函数</a></li>
<li><a href="#%E5%8D%8F%E8%AE%AE%E5%AE%9A%E4%B9%89">协议定义</a></li>
<li><a href="#%E7%B1%BB%E5%9E%8B%E5%88%AB%E5%90%8D%E4%B8%8E%E8%BE%85%E5%8A%A9%E5%87%BD%E6%95%B0">类型别名与辅助函数</a></li>
<li><a href="#statenodespec-%E5%91%BD%E5%90%8D%E5%85%83%E7%BB%84">StateNodeSpec 命名元组</a></li>
<li><a href="#stategraph-%E7%B1%BB">StateGraph 类</a></li>
<li><a href="#compiledstategraph-%E7%B1%BB">CompiledStateGraph 类</a></li>
<li><a href="#%E8%BE%85%E5%8A%A9%E5%87%BD%E6%95%B0">辅助函数</a></li>
</ol>
<hr />
<h2>导入部分</h2>
<pre><code>from __future__ import annotations
# 启用延迟类型注解评估，允许在类型提示中使用前向引用

import inspect
# 用于检查函数签名和参数信息

import logging
# 用于日志记录

import typing
# 用于类型检查和类型操作

import warnings
# 用于发出警告信息

from collections import defaultdict
# 提供默认值字典，用于自动创建缺失的键

from collections.abc import Awaitable, Hashable, Sequence
# 抽象基类：Awaitable(可等待对象)、Hashable(可哈希对象)、Sequence(序列)

from functools import partial
# 用于创建偏函数（部分应用函数）

from inspect import isclass, isfunction, ismethod, signature
# isclass: 检查是否为类
# isfunction: 检查是否为函数
# ismethod: 检查是否为方法
# signature: 获取函数签名

from types import FunctionType
# 函数类型

from typing import (
    Any,           # 任意类型
    Callable,      # 可调用对象类型
    Generic,       # 泛型基类
    Literal,       # 字面量类型
    NamedTuple,    # 命名元组
    Protocol,      # 协议类型（结构子类型）
    Union,         # 联合类型
    cast,          # 类型转换函数
    get_args,      # 获取泛型参数
    get_origin,     # 获取泛型原始类型
    get_type_hints, # 获取类型提示
    overload,      # 函数重载装饰器
)

from langchain_core.runnables import Runnable, RunnableConfig
# Runnable: 可运行对象接口
# RunnableConfig: 运行配置对象

from pydantic import BaseModel, TypeAdapter
# BaseModel: Pydantic 基础模型类
# TypeAdapter: 类型适配器，用于类型转换和验证

from typing_extensions import Self, TypeAlias, Unpack, is_typeddict
# Self: 表示当前类自身的类型
# TypeAlias: 类型别名
# Unpack: 解包操作符
# is_typeddict: 检查是否为 TypedDict

from langgraph._typing import UNSET, DeprecatedKwargs
# UNSET: 未设置值的标记
# DeprecatedKwargs: 已弃用的关键字参数类型

from langgraph.cache.base import BaseCache
# 缓存基类

from langgraph.channels.base import BaseChannel
# 通道基类，用于节点间通信
# 💡 通俗理解：通道就像是一个"共享邮箱"，节点可以把数据放进去，其他节点可以从里面读取。每个状态字段都有自己的"邮箱"（通道）。

from langgraph.channels.binop import BinaryOperatorAggregate
# 二元操作符聚合通道

from langgraph.channels.ephemeral_value import EphemeralValue
# 临时值通道

from langgraph.channels.last_value import LastValue, LastValueAfterFinish
# LastValue: 最后值通道
# LastValueAfterFinish: 完成后最后值通道
# 💡 通俗理解：LastValue 就像是一个"黑板"，每次写入都会覆盖之前的内容，只保留最新的值。LastValueAfterFinish 则是等所有节点都完成后才更新。

from langgraph.channels.named_barrier_value import (
    NamedBarrierValue,
    NamedBarrierValueAfterFinish,
)
# NamedBarrierValue: 命名屏障值通道（用于等待多个节点完成）
# NamedBarrierValueAfterFinish: 完成后命名屏障值通道
# 💡 通俗理解：这就像是一个"集合点"，多个节点必须都到达这里，才能继续执行下一个节点。就像团队开会，必须所有人都到齐才能开始。

from langgraph.checkpoint.base import Checkpoint
# 检查点基类，用于保存和恢复图执行状态
# 💡 通俗理解：检查点就像游戏存档，可以保存当前进度，如果出错了可以回到之前的状态继续执行，不用从头开始。

from langgraph.constants import (
    EMPTY_SEQ,    # 空序列常量
    END,          # 结束节点标识
    INTERRUPT,    # 中断标识
    MISSING,      # 缺失值标识
    NS_END,       # 命名空间结束符
    NS_SEP,       # 命名空间分隔符
    START,        # 开始节点标识
    TAG_HIDDEN,   # 隐藏标签
    TASKS,        # 任务标识
)

from langgraph.errors import (
    ErrorCode,           # 错误代码
    InvalidUpdateError,  # 无效更新错误
    ParentCommand,       # 父命令异常
    create_error_message, # 创建错误消息
)

from langgraph.graph.branch import Branch
# 分支类，用于条件边

from langgraph.managed.base import (
    ManagedValueSpec,  # 托管值规范
    is_managed_value,  # 检查是否为托管值
)

from langgraph.pregel import Pregel
# Pregel 图执行引擎基类
# 💡 通俗理解：Pregel 是 Google 提出的图计算模型，LangGraph 用它来实际执行图。就像是一个"调度器"，负责决定哪个节点什么时候执行，如何传递数据。

from langgraph.pregel.read import ChannelRead, PregelNode
# ChannelRead: 通道读取器
# PregelNode: Pregel 节点

from langgraph.pregel.write import (
    ChannelWrite,          # 通道写入器
    ChannelWriteEntry,     # 通道写入条目
    ChannelWriteTupleEntry, # 通道写入元组条目
)

from langgraph.store.base import BaseStore
# 存储基类

from langgraph.types import (
    All,          # 全部标识
    CachePolicy,  # 缓存策略
    Checkpointer, # 检查点器
    Command,      # 命令类型
    RetryPolicy,  # 重试策略
    Send,         # 发送操作
    StreamWriter, # 流写入器
)

from langgraph.typing import InputT, OutputT, StateT, StateT_contra
# InputT: 输入类型变量
# OutputT: 输出类型变量
# StateT: 状态类型变量
# StateT_contra: 状态类型逆变变量

from langgraph.utils.fields import (
    get_cached_annotated_keys,  # 获取缓存的注解键
    get_field_default,           # 获取字段默认值
    get_update_as_tuples,       # 获取更新元组
)

from langgraph.utils.pydantic import create_model
# 创建 Pydantic 模型

from langgraph.utils.runnable import coerce_to_runnable
# 强制转换为可运行对象

from langgraph.warnings import LangGraphDeprecatedSinceV05
# LangGraph 弃用警告

logger = logging.getLogger(__name__)
# 创建日志记录器
</code></pre>
<hr />
<h2>工具函数</h2>
<h3><code>_warn_invalid_state_schema</code></h3>
<pre><code>def _warn_invalid_state_schema(schema: type[Any] | Any) -&gt; None:
    """
    警告无效的状态模式。
    
    如果模式不是类型，且没有泛型参数，则发出警告。
    这有助于用户发现状态模式定义错误。
    """
    if isinstance(schema, type):
        # 如果是类型，直接返回（有效）
        return
    if typing.get_args(schema):
        # 如果有泛型参数（如 Annotated），也认为是有效的
        return
    # 否则发出警告
    warnings.warn(
        f"Invalid state_schema: {schema}. Expected a type or Annotated[type, reducer]. "
        "Please provide a valid schema to ensure correct updates.\n"
        " See: https://langchain-ai.github.io/langgraph/reference/graphs/#stategraph"
    )
</code></pre>
<hr />
<h2>协议定义</h2>
<p>这些协议定义了节点函数的不同签名变体，支持不同的参数组合：</p>
<blockquote>
<p>💡 <strong>通俗理解</strong>：协议就像是一份"合同"，规定了节点函数可以有哪些不同的"签名"（参数组合）。有些节点只需要状态，有些还需要配置、写入器或存储。就像不同的工作岗位有不同的职责要求一样。</p>
</blockquote>
<h3><code>_StateNode</code></h3>
<pre><code>class _StateNode(Protocol[StateT_contra]):
    """最基本的节点协议：只接受状态参数"""
    def __call__(self, state: StateT_contra) -&gt; Any: ...
</code></pre>
<h3><code>_NodeWithConfig</code></h3>
<pre><code>class _NodeWithConfig(Protocol[StateT_contra]):
    """带配置的节点协议：接受状态和配置参数"""
    def __call__(self, state: StateT_contra, config: RunnableConfig) -&gt; Any: ...
</code></pre>
<h3><code>_NodeWithWriter</code></h3>
<pre><code>class _NodeWithWriter(Protocol[StateT_contra]):
    """带写入器的节点协议：接受状态和流写入器"""
    def __call__(self, state: StateT_contra, *, writer: StreamWriter) -&gt; Any: ...
</code></pre>
<h3><code>_NodeWithStore</code></h3>
<pre><code>class _NodeWithStore(Protocol[StateT_contra]):
    """带存储的节点协议：接受状态和存储对象"""
    def __call__(self, state: StateT_contra, *, store: BaseStore) -&gt; Any: ...
</code></pre>
<h3><code>_NodeWithWriterStore</code></h3>
<pre><code>class _NodeWithWriterStore(Protocol[StateT_contra]):
    """带写入器和存储的节点协议"""
    def __call__(
        self, state: StateT_contra, *, writer: StreamWriter, store: BaseStore
    ) -&gt; Any: ...
</code></pre>
<h3><code>_NodeWithConfigWriter</code></h3>
<pre><code>class _NodeWithConfigWriter(Protocol[StateT_contra]):
    """带配置和写入器的节点协议"""
    def __call__(
        self, state: StateT_contra, *, config: RunnableConfig, writer: StreamWriter
    ) -&gt; Any: ...
</code></pre>
<h3><code>_NodeWithConfigStore</code></h3>
<pre><code>class _NodeWithConfigStore(Protocol[StateT_contra]):
    """带配置和存储的节点协议"""
    def __call__(
        self, state: StateT_contra, *, config: RunnableConfig, store: BaseStore
    ) -&gt; Any: ...
</code></pre>
<h3><code>_NodeWithConfigWriterStore</code></h3>
<pre><code>class _NodeWithConfigWriterStore(Protocol[StateT_contra]):
    """带配置、写入器和存储的节点协议（最完整的签名）"""
    def __call__(
        self,
        state: StateT_contra,
        *,
        config: RunnableConfig,
        writer: StreamWriter,
        store: BaseStore,
    ) -&gt; Any: ...
</code></pre>
<hr />
<h2>类型别名与辅助函数</h2>
<h3><code>StateNode</code> 类型别名</h3>
<pre><code>StateNode: TypeAlias = Union[
    _StateNode[StateT_contra],              # 基础节点
    _NodeWithConfig[StateT_contra],         # 带配置
    _NodeWithWriter[StateT_contra],         # 带写入器
    _NodeWithStore[StateT_contra],          # 带存储
    _NodeWithWriterStore[StateT_contra],    # 带写入器和存储
    _NodeWithConfigWriter[StateT_contra],   # 带配置和写入器
    _NodeWithConfigStore[StateT_contra],    # 带配置和存储
    _NodeWithConfigWriterStore[StateT_contra], # 带所有参数
    Runnable[StateT_contra, Any],           # 可运行对象
]
</code></pre>
<p><strong>说明</strong>：<code>StateNode</code> 是所有支持的节点类型的联合类型，允许节点函数有不同的签名。</p>
<blockquote>
<p>💡 <strong>通俗理解</strong>：<code>StateNode</code> 就像是一个"万能接口"，不管你写节点函数时用了哪些参数（只要符合其中一种协议），都可以被 LangGraph 识别和使用。这给了开发者很大的灵活性。</p>
</blockquote>
<h3><code>_get_node_name</code> 函数</h3>
<pre><code>def _get_node_name(node: StateNode) -&gt; str:
    """
    获取节点名称。
    
    尝试从节点的 __name__ 属性获取，如果没有则使用类名。
    如果都获取不到，抛出类型错误。
    """
    try:
        return getattr(node, "__name__", node.__class__.__name__)
    except AttributeError:
        raise TypeError(f"Unsupported node type: {type(node)}")
</code></pre>
<hr />
<h2>StateNodeSpec 命名元组</h2>
<blockquote>
<p>💡 <strong>通俗理解</strong>：<code>StateNodeSpec</code> 就像是节点的"身份证"，记录了节点的所有信息：它是什么（runnable）、需要什么输入（input）、失败后怎么办（retry_policy）、结果可以缓存吗（cache_policy）、执行完后可以去哪里（ends）等等。</p>
</blockquote>
<pre><code>class StateNodeSpec(NamedTuple):
    """
    节点规范，存储节点的所有配置信息。
    
    字段说明：
    - runnable: 节点可运行对象（函数或 Runnable）
    - metadata: 节点元数据字典（可选）
    - input: 节点输入模式类型
    - retry_policy: 重试策略（可选，可以是单个策略或策略序列）
    - cache_policy: 缓存策略（可选）
    - ends: 节点可以路由到的目标节点（可选）
        可以是元组（节点名列表）或字典（节点名到标签的映射）
    - defer: 是否延迟执行（直到运行即将结束时才执行）
    """
    runnable: StateNode
    metadata: dict[str, Any] | None
    input: type[Any]
    retry_policy: RetryPolicy | Sequence[RetryPolicy] | None
    cache_policy: CachePolicy | None
    ends: tuple[str, ...] | dict[str, str] | None = EMPTY_SEQ
    defer: bool = False
</code></pre>
<hr />
<h2>StateGraph 类</h2>
<p><code>StateGraph</code> 是 LangGraph 的核心类，用于构建基于状态的有向图。</p>
<blockquote>
<p>💡 <strong>通俗理解</strong>：<code>StateGraph</code> 就像是一个"建筑图纸"，你用它来设计工作流程。你可以添加节点（工作步骤）、连接边（执行顺序）、设置条件分支（根据情况选择路径）。设计完成后，调用 <code>compile()</code> 方法把它"建造"成可以实际运行的图。</p>
</blockquote>
<h3>类属性</h3>
<pre><code>edges: set[tuple[str, str]]
# 边的集合，每个边是 (起始节点, 结束节点) 的元组

nodes: dict[str, StateNodeSpec]
# 节点字典，键是节点名，值是节点规范

branches: defaultdict[str, dict[str, Branch]]
# 分支字典，键是源节点名，值是条件名到分支对象的映射

channels: dict[str, BaseChannel]
# 通道字典，键是状态键名，值是通道对象
# 💡 通俗理解：每个状态字段都有自己的"通道"（邮箱），节点通过通道读写数据

managed: dict[str, ManagedValueSpec]
# 托管值字典，键是状态键名，值是托管值规范

schemas: dict[type[Any], dict[str, BaseChannel | ManagedValueSpec]]
# 模式字典，键是模式类型，值是键到通道或托管值的映射

waiting_edges: set[tuple[tuple[str, ...], str]]
# 等待边集合，用于多源节点到单目标节点的边
# 💡 通俗理解：就像"等所有人到齐再出发"，多个节点必须都完成，才能执行下一个节点

compiled: bool
# 是否已编译的标志

state_schema: type[StateT]
# 状态模式类型

input_schema: type[InputT]
# 输入模式类型

output_schema: type[OutputT]
# 输出模式类型
</code></pre>
<h3><code>__init__</code> 方法</h3>
<pre><code>def __init__(
    self,
    state_schema: type[StateT],           # 状态模式（必需）
    config_schema: type[Any] | None = None, # 配置模式（可选）
    *,
    input_schema: type[InputT] | None = None,  # 输入模式（可选）
    output_schema: type[OutputT] | None = None, # 输出模式（可选）
    **kwargs: Unpack[DeprecatedKwargs],   # 已弃用的关键字参数
) -&gt; None:
    """
    初始化状态图。
    
    处理已弃用的参数（input/output），然后初始化所有数据结构。
    最后添加状态、输入和输出模式。
    """
    
    &gt; 💡 **通俗理解**：初始化就像准备一个空的工作台，设置好工作台的规格（状态模式），准备好各种"邮箱"（通道）。之后你就可以往上面添加工作步骤（节点）了。
    
    # 处理已弃用的 input 参数
    if (input_ := kwargs.get("input", UNSET)) is not UNSET:
        warnings.warn(
            "`input` is deprecated and will be removed. Please use `input_schema` instead.",
            category=LangGraphDeprecatedSinceV05,
            stacklevel=2,
        )
        if input_schema is None:
            input_schema = cast(Union[type[InputT], None], input_)

    # 处理已弃用的 output 参数
    if (output := kwargs.get("output", UNSET)) is not UNSET:
        warnings.warn(
            "`output` is deprecated and will be removed. Please use `output_schema` instead.",
            category=LangGraphDeprecatedSinceV05,
            stacklevel=2,
        )
        if output_schema is None:
            output_schema = cast(Union[type[OutputT], None], output)

    # 初始化数据结构
    self.nodes = {}
    self.edges = set()
    self.branches = defaultdict(dict)
    self.schemas = {}
    self.channels = {}
    self.managed = {}
    self.compiled = False
    self.waiting_edges = set()

    # 设置模式
    self.state_schema = state_schema
    self.input_schema = cast(type[InputT], input_schema or state_schema)
    self.output_schema = cast(type[OutputT], output_schema or state_schema)
    self.config_schema = config_schema

    # 添加模式（这会解析模式并创建通道）
    self._add_schema(self.state_schema)
    self._add_schema(self.input_schema, allow_managed=False)
    self._add_schema(self.output_schema, allow_managed=False)
</code></pre>
<h3><code>_all_edges</code> 属性</h3>
<pre><code>@property
def _all_edges(self) -&gt; set[tuple[str, str]]:
    """
    获取所有边的集合（包括普通边和等待边）。
    
    等待边会被展开为多个普通边。
    """
    return self.edges | {
        (start, end) for starts, end in self.waiting_edges for start in starts
    }
</code></pre>
<h3><code>_add_schema</code> 方法</h3>
<pre><code>def _add_schema(self, schema: type[Any], /, allow_managed: bool = True) -&gt; None:
    """
    添加模式到图中。
    
    解析模式中的字段，创建相应的通道和托管值。
    如果模式已存在，则跳过。
    
    参数：
    - schema: 要添加的模式类型
    - allow_managed: 是否允许托管值（输入/输出模式不允许）
    """
    
    &gt; 💡 **通俗理解**：模式就像是一个"表格模板"，定义了状态有哪些字段、每个字段是什么类型。这个方法会为每个字段创建一个"邮箱"（通道），节点可以通过这些邮箱传递数据。`allow_managed=False` 的意思是：输入/输出模式不允许使用"托管值"（一种特殊的通道类型，由系统自动管理）。
    
    if schema not in self.schemas:
        # 警告无效的模式
        _warn_invalid_state_schema(schema)
        # 获取通道、托管值和类型提示
        channels, managed, type_hints = _get_channels(schema)
        
        # 检查托管值是否允许
        if managed and not allow_managed:
            names = ", ".join(managed)
            schema_name = getattr(schema, "__name__", "")
            raise ValueError(
                f"Invalid managed channels detected in {schema_name}: {names}."
                " Managed channels are not permitted in Input/Output schema."
            )
        
        # 保存模式信息
        self.schemas[schema] = {**channels, **managed}
        
        # 添加通道
        for key, channel in channels.items():
            if key in self.channels:
                if self.channels[key] != channel:
                    # 如果通道类型不同，且不是 LastValue，则报错
                    if isinstance(channel, LastValue):
                        pass  # LastValue 可以兼容
                    else:
                        raise ValueError(
                            f"Channel '{key}' already exists with a different type"
                        )
            else:
                self.channels[key] = channel
        
        # 添加托管值
        for key, managed in managed.items():
            if key in self.managed:
                if self.managed[key] != managed:
                    raise ValueError(
                        f"Managed value '{key}' already exists with a different type"
                    )
            else:
                self.managed[key] = managed
</code></pre>
<h3><code>add_node</code> 方法</h3>
<p><code>add_node</code> 方法有两个重载版本和一个实现版本：</p>
<h4>重载版本 1</h4>
<pre><code>@overload
def add_node(
    self,
    node: StateNode[StateT],  # 节点函数或可运行对象
    *,
    defer: bool = False,
    metadata: dict[str, Any] | None = None,
    input_schema: type[Any] | None = None,
    retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None,
    cache_policy: CachePolicy | None = None,
    destinations: dict[str, str] | tuple[str, ...] | None = None,
    **kwargs: Unpack[DeprecatedKwargs],
) -&gt; Self:
    """添加节点，节点名从函数/可运行对象名称推断"""
    ...
</code></pre>
<h4>重载版本 2</h4>
<pre><code>@overload
def add_node(
    self,
    node: str,              # 节点名称
    action: StateNode[StateT], # 节点函数或可运行对象
    *,
    defer: bool = False,
    metadata: dict[str, Any] | None = None,
    input_schema: type[Any] | None = None,
    retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None,
    cache_policy: CachePolicy | None = None,
    destinations: dict[str, str] | tuple[str, ...] | None = None,
    **kwargs: Unpack[DeprecatedKwargs],
) -&gt; Self:
    """添加节点，显式指定节点名"""
    ...
</code></pre>
<h4>实现版本</h4>
<pre><code>def add_node(
    self,
    node: str | StateNode[StateT],
    action: StateNode[StateT] | None = None,
    *,
    defer: bool = False,
    metadata: dict[str, Any] | None = None,
    input_schema: type[Any] | None = None,
    retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None,
    cache_policy: CachePolicy | None = None,
    destinations: dict[str, str] | tuple[str, ...] | None = None,
    **kwargs: Unpack[DeprecatedKwargs],
) -&gt; Self:
    """
    添加节点到图中。
    
    主要步骤：
    1. 处理已弃用的参数
    2. 确定节点名称和动作
    3. 验证节点名称
    4. 推断输入模式和目标节点（从类型提示）
    5. 创建节点规范并添加到图中
    """
    
    &gt; 💡 **通俗理解**：添加节点就像是在工作流程中添加一个工作步骤。这个方法会：
    &gt; - 检查节点名是否合法（不能是保留字，不能重复）
    &gt; - 自动识别节点函数的类型提示，推断它需要什么输入、可以输出到哪里
    &gt; - 创建一个"身份证"（StateNodeSpec）记录这个节点的所有信息
    &gt; - 把节点添加到图中
    
    # 处理已弃用的 retry 参数
    if (retry := kwargs.get("retry", UNSET)) is not UNSET:
        warnings.warn(
            "`retry` is deprecated and will be removed. Please use `retry_policy` instead.",
            category=LangGraphDeprecatedSinceV05,
        )
        if retry_policy is None:
            retry_policy = retry

    # 处理已弃用的 input 参数
    if (input_ := kwargs.get("input", UNSET)) is not UNSET:
        warnings.warn(
            "`input` is deprecated and will be removed. Please use `input_schema` instead.",
            category=LangGraphDeprecatedSinceV05,
        )
        if input_schema is None:
            input_schema = cast(Union[type[InputT], None], input_)

    # 确定节点名称和动作
    if not isinstance(node, str):
        action = node
        if isinstance(action, Runnable):
            node = action.get_name()
        else:
            node = getattr(action, "__name__", action.__class__.__name__)
        if node is None:
            raise ValueError(
                "Node name must be provided if action is not a function"
            )
    
    # 如果图已编译，发出警告
    if self.compiled:
        logger.warning(
            "Adding a node to a graph that has already been compiled. This will "
            "not be reflected in the compiled graph."
        )
    
    # 再次检查节点名称（处理边界情况）
    if not isinstance(node, str):
        action = node
        node = cast(str, getattr(action, "name", getattr(action, "__name__", None)))
        if node is None:
            raise ValueError(
                "Node name must be provided if action is not a function"
            )
    
    # 验证动作存在
    if action is None:
        raise RuntimeError
    
    # 验证节点名唯一性
    if node in self.nodes:
        raise ValueError(f"Node `{node}` already present.")
    
    # 验证节点名不是保留字
    if node == END or node == START:
        raise ValueError(f"Node `{node}` is reserved.")

    # 验证节点名不包含保留字符
    for character in (NS_SEP, NS_END):
        if character in node:
            raise ValueError(
                f"'{character}' is a reserved character and is not allowed in the node names."
            )

    # 尝试从类型提示推断目标节点
    ends: tuple[str, ...] | dict[str, str] = EMPTY_SEQ
    try:
        if (
            isfunction(action)
            or ismethod(action)
            or ismethod(getattr(action, "__call__", None))
        ) and (
            hints := get_type_hints(getattr(action, "__call__"))
            or get_type_hints(action)
        ):
            # 推断输入模式
            if input_schema is None:
                first_parameter_name = next(
                    iter(
                        inspect.signature(
                            cast(FunctionType, action)
                        ).parameters.keys()
                    )
                )
                if input_hint := hints.get(first_parameter_name):
                    if isinstance(input_hint, type) and get_type_hints(input_hint):
                        input_schema = input_hint
            
            # 推断返回类型中的目标节点
            if rtn := hints.get("return"):
                # 处理 Union 类型
                rtn_origin = get_origin(rtn)
                if rtn_origin is Union:
                    rtn_args = get_args(rtn)
                    # 查找 Command 类型
                    for arg in rtn_args:
                        arg_origin = get_origin(arg)
                        if arg_origin is Command:
                            rtn = arg
                            rtn_origin = arg_origin
                            break

                # 检查是否为 Command 类型
                if (
                    rtn_origin is Command
                    and (rargs := get_args(rtn))
                    and get_origin(rargs[0]) is Literal
                    and (vals := get_args(rargs[0]))
                ):
                    ends = vals
    except (NameError, TypeError, StopIteration):
        pass

    # 如果提供了 destinations，使用它
    if destinations is not None:
        ends = destinations

    # 添加输入模式（如果指定）
    if input_schema is not None:
        self._add_schema(input_schema)
    
    # 创建节点规范并添加到图中
    self.nodes[node] = StateNodeSpec(
        coerce_to_runnable(action, name=node, trace=False),
        metadata,
        input=input_schema or self.state_schema,
        retry_policy=retry_policy,
        cache_policy=cache_policy,
        ends=ends,
        defer=defer,
    )
    return self
</code></pre>
<h3><code>add_edge</code> 方法</h3>
<pre><code>def add_edge(self, start_key: str | list[str], end_key: str) -&gt; Self:
    """
    添加有向边到图中。
    
    支持两种模式：
    1. 单源到单目标：start_key 是字符串
    2. 多源到单目标：start_key 是字符串列表（等待所有源节点完成）
    
    参数：
    - start_key: 起始节点（或节点列表）
    - end_key: 结束节点
    
    返回：
    - Self: 允许方法链式调用
    """
    
    &gt; 💡 **通俗理解**：添加边就像是在工作流程中画箭头，表示"先做这个，再做那个"。
    &gt; - 单源边：A → B（A 完成后执行 B）
    &gt; - 多源边：[A, B, C] → D（A、B、C 都完成后才执行 D，就像等所有同事都到齐再开会）
    
    if self.compiled:
        logger.warning(
            "Adding an edge to a graph that has already been compiled. This will "
            "not be reflected in the compiled graph."
        )

    # 处理单源情况
    if isinstance(start_key, str):
        if start_key == END:
            raise ValueError("END cannot be a start node")
        if end_key == START:
            raise ValueError("START cannot be an end node")

        # 验证（仅对非 StateGraph 图）
        if not hasattr(self, "channels") and start_key in set(
            start for start, _ in self.edges
        ):
            raise ValueError(
                f"Already found path for node '{start_key}'.\n"
                "For multiple edges, use StateGraph with an Annotated state key."
            )

        self.edges.add((start_key, end_key))
        return self

    # 处理多源情况
    for start in start_key:
        if start == END:
            raise ValueError("END cannot be a start node")
        if start not in self.nodes:
            raise ValueError(f"Need to add_node `{start}` first")
    if end_key == START:
        raise ValueError("START cannot be an end node")
    if end_key != END and end_key not in self.nodes:
        raise ValueError(f"Need to add_node `{end_key}` first")

    # 添加到等待边集合
    self.waiting_edges.add((tuple(start_key), end_key))
    return self
</code></pre>
<h3><code>add_conditional_edges</code> 方法</h3>
<pre><code>def add_conditional_edges(
    self,
    source: str,  # 源节点
    path: Callable[..., Hashable | list[Hashable]]
    | Callable[..., Awaitable[Hashable | list[Hashable]]]
    | Runnable[Any, Hashable | list[Hashable]],  # 路径函数
    path_map: dict[Hashable, str] | list[str] | None = None,  # 路径映射
) -&gt; Self:
    """
    添加条件边。
    
    条件边允许根据状态动态选择下一个节点。
    
    参数：
    - source: 源节点名
    - path: 路径函数，根据状态返回下一个节点名（或节点名列表）
    - path_map: 可选的路径映射，将路径函数的返回值映射到节点名
    
    返回：
    - Self: 允许方法链式调用
    """
    
    &gt; 💡 **通俗理解**：条件边就像是一个"智能路口"，根据当前情况（状态）决定走哪条路。
    &gt; - `path` 函数就像是一个"导航员"，查看当前状态后决定下一步去哪里
    &gt; - `path_map` 就像一个"翻译表"，把导航员的决定（可能是数字、字符串）翻译成实际的节点名
    &gt; - 例如：如果用户输入是问题，就去"回答节点"；如果是命令，就去"执行节点"
    
    if self.compiled:
        logger.warning(
            "Adding an edge to a graph that has already been compiled. This will "
            "not be reflected in the compiled graph."
        )

    # 将路径函数转换为可运行对象
    path = coerce_to_runnable(path, name=None, trace=True)
    name = path.name or "condition"
    
    # 验证条件名唯一性
    if name in self.branches[source]:
        raise ValueError(
            f"Branch with name `{path.name}` already exists for node `{source}`"
        )
    
    # 创建分支并保存
    self.branches[source][name] = Branch.from_path(path, path_map, True)
    
    # 如果分支有输入模式，添加它
    if schema := self.branches[source][name].input_schema:
        self._add_schema(schema)
    return self
</code></pre>
<h3><code>add_sequence</code> 方法</h3>
<pre><code>def add_sequence(
    self,
    nodes: Sequence[StateNode[StateT] | tuple[str, StateNode[StateT]]],
) -&gt; Self:
    """
    添加节点序列。
    
    按顺序添加多个节点，并在它们之间创建边。
    
    参数：
    - nodes: 节点序列，可以是 StateNode 或 (名称, StateNode) 元组
    
    返回：
    - Self: 允许方法链式调用
    """
    if len(nodes) &lt; 1:
        raise ValueError("Sequence requires at least one node.")

    previous_name: str | None = None
    for node in nodes:
        # 确定节点名称
        if isinstance(node, tuple) and len(node) == 2:
            name, node = node
        else:
            name = _get_node_name(node)

        # 验证节点名唯一性
        if name in self.nodes:
            raise ValueError(
                f"Node names must be unique: node with the name '{name}' already exists. "
                "If you need to use two different runnables/callables with the same name (for example, using `lambda`), please provide them as tuples (name, runnable/callable)."
            )

        # 添加节点
        self.add_node(name, node)
        
        # 如果不是第一个节点，添加边
        if previous_name is not None:
            self.add_edge(previous_name, name)

        previous_name = name

    return self
</code></pre>
<h3><code>set_entry_point</code> 方法</h3>
<pre><code>def set_entry_point(self, key: str) -&gt; Self:
    """
    设置图的入口点。
    
    等价于调用 add_edge(START, key)。
    
    参数：
    - key: 入口节点名
    
    返回：
    - Self: 允许方法链式调用
    """
    return self.add_edge(START, key)
</code></pre>
<h3><code>set_conditional_entry_point</code> 方法</h3>
<pre><code>def set_conditional_entry_point(
    self,
    path: Callable[..., Hashable | list[Hashable]]
    | Callable[..., Awaitable[Hashable | list[Hashable]]]
    | Runnable[Any, Hashable | list[Hashable]],
    path_map: dict[Hashable, str] | list[str] | None = None,
) -&gt; Self:
    """
    设置条件入口点。
    
    允许根据输入动态选择第一个节点。
    
    参数：
    - path: 路径函数
    - path_map: 路径映射
    
    返回：
    - Self: 允许方法链式调用
    """
    return self.add_conditional_edges(START, path, path_map)
</code></pre>
<h3><code>set_finish_point</code> 方法</h3>
<pre><code>def set_finish_point(self, key: str) -&gt; Self:
    """
    设置图的结束点。
    
    等价于调用 add_edge(key, END)。
    
    参数：
    - key: 结束节点名
    
    返回：
    - Self: 允许方法链式调用
    """
    return self.add_edge(key, END)
</code></pre>
<h3><code>validate</code> 方法</h3>
<pre><code>def validate(self, interrupt: Sequence[str] | None = None) -&gt; Self:
    """
    验证图的结构。
    
    检查：
    1. 所有边的源节点都存在
    2. 图有入口点
    3. 所有边的目标节点都存在
    4. 中断节点存在
    
    参数：
    - interrupt: 中断节点列表（可选）
    
    返回：
    - Self: 允许方法链式调用
    
    副作用：
    - 设置 self.compiled = True
    """
    
    &gt; 💡 **通俗理解**：验证就像是在"施工前检查图纸"，确保：
    &gt; - 所有箭头都指向存在的节点（不能指向空）
    &gt; - 有起点（不能没有入口）
    &gt; - 所有目标节点都存在（不能指向不存在的节点）
    &gt; - 如果设置了中断点，这些节点必须存在
    &gt; 如果检查通过，就标记为"已验证"，可以编译了。
    
    # 收集所有源节点
    all_sources = {src for src, _ in self._all_edges}
    for start, branches in self.branches.items():
        all_sources.add(start)
    for name, spec in self.nodes.items():
        if spec.ends:
            all_sources.add(name)
    
    # 验证源节点
    for source in all_sources:
        if source not in self.nodes and source != START:
            raise ValueError(f"Found edge starting at unknown node '{source}'")

    # 验证有入口点
    if START not in all_sources:
        raise ValueError(
            "Graph must have an entrypoint: add at least one edge from START to another node"
        )

    # 收集所有目标节点
    all_targets = {end for _, end in self._all_edges}
    for start, branches in self.branches.items():
        for cond, branch in branches.items():
            if branch.ends is not None:
                for end in branch.ends.values():
                    if end not in self.nodes and end != END:
                        raise ValueError(
                            f"At '{start}' node, '{cond}' branch found unknown target '{end}'"
                        )
                    all_targets.add(end)
            else:
                # 如果没有指定目标，可能路由到任何节点
                all_targets.add(END)
                for node in self.nodes:
                    if node != start:
                        all_targets.add(node)
    for name, spec in self.nodes.items():
        if spec.ends:
            all_targets.update(spec.ends)
    
    # 验证目标节点
    for target in all_targets:
        if target not in self.nodes and target != END:
            raise ValueError(f"Found edge ending at unknown node `{target}`")
    
    # 验证中断节点
    if interrupt:
        for node in interrupt:
            if node not in self.nodes:
                raise ValueError(f"Interrupt node `{node}` not found")

    self.compiled = True
    return self
</code></pre>
<h3><code>compile</code> 方法</h3>
<pre><code>def compile(
    self,
    checkpointer: Checkpointer = None,  # 检查点器
    *,
    cache: BaseCache | None = None,     # 缓存
    store: BaseStore | None = None,     # 存储
    interrupt_before: All | list[str] | None = None,  # 执行前中断的节点
    interrupt_after: All | list[str] | None = None,   # 执行后中断的节点
    debug: bool = False,                 # 调试模式
    name: str | None = None,            # 图名称
) -&gt; CompiledStateGraph[StateT, InputT, OutputT]:
    """
    编译状态图为 CompiledStateGraph。
    
    编译后的图实现了 Runnable 接口，可以调用、流式传输、批处理和异步运行。
    
    主要步骤：
    1. 验证图结构
    2. 准备输出通道和流通道
    3. 创建 CompiledStateGraph 实例
    4. 附加所有节点、边和分支
    
    返回：
    - CompiledStateGraph: 编译后的状态图
    """
    
    &gt; 💡 **通俗理解**：编译就像把"设计图纸"变成"实际可运行的机器"。
    &gt; - 先检查图纸有没有问题（validate）
    &gt; - 准备好输出接口（output_channels）和流式接口（stream_channels）
    &gt; - 创建一个"执行引擎"（CompiledStateGraph）
    &gt; - 把所有节点、边、分支都"安装"到引擎上
    &gt; 编译完成后，你就可以调用 `invoke()` 或 `stream()` 来运行这个图了！
    
    # 设置默认值
    interrupt_before = interrupt_before or []
    interrupt_after = interrupt_after or []

    # 验证图
    self.validate(
        interrupt=(
            (interrupt_before if interrupt_before != "*" else []) + interrupt_after
            if interrupt_after != "*"
            else []
        )
    )

    # 准备输出通道
    output_channels = (
        "__root__"
        if len(self.schemas[self.output_schema]) == 1
        and "__root__" in self.schemas[self.output_schema]
        else [
            key
            for key, val in self.schemas[self.output_schema].items()
            if not is_managed_value(val)
        ]
    )
    
    # 准备流通道
    stream_channels = (
        "__root__"
        if len(self.channels) == 1 and "__root__" in self.channels
        else [
            key for key, val in self.channels.items() if not is_managed_value(val)
        ]
    )

    # 创建编译后的图
    compiled = CompiledStateGraph[StateT, InputT, OutputT](
        builder=self,
        schema_to_mapper={},
        config_type=self.config_schema,
        nodes={},
        channels={
            **self.channels,
            **self.managed,
            START: EphemeralValue(self.input_schema),
        },
        input_channels=START,
        stream_mode="updates",
        output_channels=output_channels,
        stream_channels=stream_channels,
        checkpointer=checkpointer,
        interrupt_before_nodes=interrupt_before,
        interrupt_after_nodes=interrupt_after,
        auto_validate=False,
        debug=debug,
        store=store,
        cache=cache,
        name=name or "LangGraph",
    )

    # 附加 START 节点
    compiled.attach_node(START, None)
    
    # 附加所有节点
    for key, node in self.nodes.items():
        compiled.attach_node(key, node)

    # 附加所有边
    for start, end in self.edges:
        compiled.attach_edge(start, end)

    # 附加所有等待边
    for starts, end in self.waiting_edges:
        compiled.attach_edge(starts, end)

    # 附加所有分支
    for start, branches in self.branches.items():
        for name, branch in branches.items():
            compiled.attach_branch(start, name, branch)

    return compiled.validate()
</code></pre>
<hr />
<h2>CompiledStateGraph 类</h2>
<p><code>CompiledStateGraph</code> 是编译后的状态图，继承自 <code>Pregel</code>，实现了实际的图执行逻辑。</p>
<blockquote>
<p>💡 <strong>通俗理解</strong>：<code>CompiledStateGraph</code> 就是"实际运行的机器"，而 <code>StateGraph</code> 只是"设计图纸"。</p>
<ul>
<li><code>StateGraph</code>：你用它来设计工作流程（添加节点、边等）</li>
<li><code>CompiledStateGraph</code>：编译后得到的可执行对象，你可以调用它来实际运行工作流程
就像写代码和运行代码的区别一样。</li>
</ul>
</blockquote>
<h3>类属性</h3>
<pre><code>class CompiledStateGraph(
    Pregel[StateT, InputT, OutputT], Generic[StateT, InputT, OutputT]
):
    """
    编译后的状态图。
    
    继承自 Pregel，实现了图的执行逻辑。
    """
    builder: StateGraph[StateT, InputT, OutputT]
    # 构建器引用，保存原始的状态图定义
    
    schema_to_mapper: dict[type[Any], Callable[[Any], Any] | None]
    # 模式到映射函数的字典，用于将状态字典转换为模式对象
</code></pre>
<h3><code>__init__</code> 方法</h3>
<pre><code>def __init__(
    self,
    *,
    builder: StateGraph[StateT, InputT, OutputT],
    schema_to_mapper: dict[type[Any], Callable[[Any], Any] | None],
    **kwargs: Any,
) -&gt; None:
    """
    初始化编译后的状态图。
    
    调用父类 Pregel 的初始化方法，然后保存构建器和模式映射器。
    """
    super().__init__(**kwargs)
    self.builder = builder
    self.schema_to_mapper = schema_to_mapper
</code></pre>
<h3><code>get_input_jsonschema</code> 方法</h3>
<pre><code>def get_input_jsonschema(
    self, config: RunnableConfig | None = None
) -&gt; dict[str, Any]:
    """
    获取输入的 JSON Schema。
    
    用于 API 文档生成和验证。
    """
    return _get_json_schema(
        typ=self.builder.input_schema,
        schemas=self.builder.schemas,
        channels=self.builder.channels,
        name=self.get_name("Input"),
    )
</code></pre>
<h3><code>get_output_jsonschema</code> 方法</h3>
<pre><code>def get_output_jsonschema(
    self, config: RunnableConfig | None = None
) -&gt; dict[str, Any]:
    """
    获取输出的 JSON Schema。
    
    用于 API 文档生成和验证。
    """
    return _get_json_schema(
        typ=self.builder.output_schema,
        schemas=self.builder.schemas,
        channels=self.builder.channels,
        name=self.get_name("Output"),
    )
</code></pre>
<h3><code>attach_node</code> 方法</h3>
<pre><code>def attach_node(self, key: str, node: StateNodeSpec | None) -&gt; None:
    """
    将节点附加到编译后的图中。
    
    这是编译过程的关键步骤，将 StateGraph 的节点转换为 Pregel 节点。
    
    主要工作：
    1. 确定输出键
    2. 创建更新函数（处理节点返回值）
    3. 创建写入条目（状态更新和分支控制）
    4. 创建 PregelNode 并添加到图中
    
    参数：
    - key: 节点键（START 或节点名）
    - node: 节点规范（START 节点为 None）
    """
    
    &gt; 💡 **通俗理解**：这个方法把"设计图纸上的节点"转换成"实际可执行的节点"。
    &gt; - 确定节点可以更新哪些状态字段（output_keys）
    &gt; - 创建一个"转换器"（_get_updates），把节点的返回值转换成状态更新
    &gt; - 创建"写入器"（write_entries），负责把更新写入状态，并控制下一步去哪里
    &gt; - 最后创建一个 PregelNode（实际执行单元）并添加到图中
    &gt; 就像把"工作步骤说明"转换成"实际的工作指令"。
    
    # 确定输出键
    if key == START:
        # START 节点输出输入模式的所有键
        output_keys = [
            k
            for k, v in self.builder.schemas[self.builder.input_schema].items()
            if not is_managed_value(v)
        ]
    else:
        # 普通节点输出所有通道和托管值
        output_keys = list(self.builder.channels) + [
            k for k, v in self.builder.managed.items()
        ]

    def _get_updates(
        input: None | dict | Any,
    ) -&gt; Sequence[tuple[str, Any]] | None:
        """
        将节点返回值转换为更新元组列表。
        
        处理多种返回类型：
        - None: 无更新
        - dict: 字典键值对
        - Command: 命令对象
        - list/tuple: 命令列表
        - 带注解的对象: 使用 get_update_as_tuples
        """
        if input is None:
            return None
        elif isinstance(input, dict):
            return [(k, v) for k, v in input.items() if k in output_keys]
        elif isinstance(input, Command):
            if input.graph == Command.PARENT:
                return None
            return [
                (k, v) for k, v in input._update_as_tuples() if k in output_keys
            ]
        elif (
            isinstance(input, (list, tuple))
            and input
            and any(isinstance(i, Command) for i in input)
        ):
            updates: list[tuple[str, Any]] = []
            for i in input:
                if isinstance(i, Command):
                    if i.graph == Command.PARENT:
                        continue
                    updates.extend(
                        (k, v) for k, v in i._update_as_tuples() if k in output_keys
                    )
                else:
                    updates.extend(_get_updates(i) or ())
            return updates
        elif (t := type(input)) and get_cached_annotated_keys(t):
            return get_update_as_tuples(input, output_keys)
        else:
            msg = create_error_message(
                message=f"Expected dict, got {input}",
                error_code=ErrorCode.INVALID_GRAPH_NODE_RETURN_VALUE,
            )
            raise InvalidUpdateError(msg)

    # 创建写入条目
    write_entries: tuple[ChannelWriteEntry | ChannelWriteTupleEntry, ...] = (
        ChannelWriteTupleEntry(
            mapper=_get_root if output_keys == ["__root__"] else _get_updates
        ),
        ChannelWriteTupleEntry(
            mapper=_control_branch,
            static=_control_static(node.ends)
            if node is not None and node.ends is not None
            else None,
        ),
    )

    # 添加节点
    if key == START:
        # START 节点
        self.nodes[key] = PregelNode(
            tags=[TAG_HIDDEN],
            triggers=[START],
            channels=START,
            writers=[ChannelWrite(write_entries)],
        )
    elif node is not None:
        # 普通节点
        input_schema = node.input if node else self.builder.state_schema
        input_channels = list(self.builder.schemas[input_schema])
        is_single_input = len(input_channels) == 1 and "__root__" in input_channels
        
        # 获取或创建映射器
        if input_schema in self.schema_to_mapper:
            mapper = self.schema_to_mapper[input_schema]
        else:
            mapper = _pick_mapper(input_channels, input_schema)
            self.schema_to_mapper[input_schema] = mapper

        # 创建分支通道
        branch_channel = CHANNEL_BRANCH_TO.format(key)
        self.channels[branch_channel] = (
            LastValueAfterFinish(Any)
            if node.defer
            else EphemeralValue(Any, guard=False)
        )
        
        # 创建 PregelNode
        self.nodes[key] = PregelNode(
            triggers=[branch_channel],
            channels=("__root__" if is_single_input else input_channels),
            mapper=mapper,
            writers=[ChannelWrite(write_entries)],
            metadata=node.metadata,
            retry_policy=node.retry_policy,
            cache_policy=node.cache_policy,
            bound=node.runnable,
        )
    else:
        raise RuntimeError
</code></pre>
<h3><code>attach_edge</code> 方法</h3>
<pre><code>def attach_edge(self, starts: str | Sequence[str], end: str) -&gt; None:
    """
    将边附加到编译后的图中。
    
    处理两种类型的边：
    1. 单源边：直接写入目标节点的分支通道
    2. 多源边：使用命名屏障值通道等待所有源节点完成
    
    参数：
    - starts: 起始节点（或节点列表）
    - end: 结束节点
    """
    
    &gt; 💡 **通俗理解**：这个方法把"设计图纸上的箭头"转换成"实际的执行路径"。
    &gt; - 单源边：A → B，直接在 A 的写入器中添加"完成后通知 B"
    &gt; - 多源边：[A, B, C] → D，创建一个"集合点通道"，A、B、C 都完成后才通知 D
    &gt; 就像把"流程图上的箭头"转换成"实际的信号传递机制"。
    
    if isinstance(starts, str):
        # 单源边：直接写入目标节点的分支通道
        if end != END:
            self.nodes[starts].writers.append(
                ChannelWrite(
                    (ChannelWriteEntry(CHANNEL_BRANCH_TO.format(end), None),)
                )
            )
    elif end != END:
        # 多源边：创建命名屏障值通道
        channel_name = f"join:{'+'.join(starts)}:{end}"
        
        # 注册通道
        if self.builder.nodes[end].defer:
            # 如果目标节点延迟执行，使用 AfterFinish 版本
            self.channels[channel_name] = NamedBarrierValueAfterFinish(
                str, set(starts)
            )
        else:
            self.channels[channel_name] = NamedBarrierValue(str, set(starts))
        
        # 订阅通道（目标节点等待）
        self.nodes[end].triggers.append(channel_name)
        
        # 发布到通道（源节点写入）
        for start in starts:
            self.nodes[start].writers.append(
                ChannelWrite((ChannelWriteEntry(channel_name, start),))
            )
</code></pre>
<h3><code>attach_branch</code> 方法</h3>
<pre><code>def attach_branch(
    self, start: str, name: str, branch: Branch, *, with_reader: bool = True
) -&gt; None:
    """
    将分支附加到编译后的图中。
    
    分支用于条件边，根据状态动态选择下一个节点。
    
    参数：
    - start: 源节点名
    - name: 分支名
    - branch: 分支对象
    - with_reader: 是否创建读取器（用于读取状态）
    """
    
    &gt; 💡 **通俗理解**：这个方法把"条件判断逻辑"转换成"实际的路由机制"。
    &gt; - 创建一个"读取器"（reader）来读取当前状态
    &gt; - 创建一个"写入器"（get_writes）来把分支决策转换成"通知哪个节点"
    &gt; - 把分支逻辑附加到源节点上
    &gt; 就像把"如果...就..."的逻辑转换成"实际的信号路由"。
    
    def get_writes(
        packets: Sequence[str | Send], static: bool = False
    ) -&gt; Sequence[ChannelWriteEntry | Send]:
        """
        将分支返回的包转换为写入条目。
        
        参数：
        - packets: 包列表（节点名或 Send 对象）
        - static: 是否为静态分支
        
        返回：
        - 写入条目列表
        """
        writes = [
            (
                ChannelWriteEntry(
                    p if p == END else CHANNEL_BRANCH_TO.format(p), None
                )
                if not isinstance(p, Send)
                else p
            )
            for p in packets
            if (True if static else p != END)
        ]
        if not writes:
            return []
        return writes

    if with_reader:
        # 获取模式
        schema = branch.input_schema or (
            self.builder.nodes[start].input
            if start in self.builder.nodes
            else self.builder.state_schema
        )
        channels = list(self.builder.schemas[schema])
        
        # 获取映射器
        if schema in self.schema_to_mapper:
            mapper = self.schema_to_mapper[schema]
        else:
            mapper = _pick_mapper(channels, schema)
            self.schema_to_mapper[schema] = mapper
        
        # 创建读取器
        reader: Callable[[RunnableConfig], Any] | None = partial(
            ChannelRead.do_read,
            select=channels[0] if channels == ["__root__"] else channels,
            fresh=True,
            mapper=mapper,
        )
    else:
        reader = None

    # 附加分支发布器
    self.nodes[start].writers.append(branch.run(get_writes, reader))
</code></pre>
<h3><code>_migrate_checkpoint</code> 方法</h3>
<pre><code>def _migrate_checkpoint(self, checkpoint: Checkpoint) -&gt; None:
    """
    迁移检查点到新的通道布局。
    
    当图的内部结构改变时（如通道命名方式改变），需要迁移旧的检查点。
    
    迁移规则：
    1. start:node -&gt; branch:to:node
    2. branch:source:condition:node -&gt; branch:to:node
    3. node -&gt; branch:to:node（对于所有目标节点）
    
    参数：
    - checkpoint: 要迁移的检查点
    """
    
    &gt; 💡 **通俗理解**：这个方法就像"游戏版本更新时的存档迁移"。
    &gt; - LangGraph 的内部实现可能会改变（比如通道的命名方式）
    &gt; - 但旧的检查点（存档）还在用旧的命名方式
    &gt; - 这个方法会自动把旧的命名转换成新的命名，让旧存档可以在新版本中使用
    &gt; 就像游戏更新后，旧存档还能继续使用一样。
    
    super()._migrate_checkpoint(checkpoint)

    values = checkpoint["channel_values"]
    versions = checkpoint["channel_versions"]
    seen = checkpoint["versions_seen"]

    # 空检查点不需要迁移
    if not versions:
        return

    # 当前版本（v3+）不需要迁移
    if checkpoint["v"] &gt;= 3:
        return

    # 迁移 1: start:node -&gt; branch:to:node
    for k in list(versions):
        if k.startswith("start:"):
            node = k.split(":")[1]
            if node not in self.nodes:
                continue
            new_k = f"branch:to:{node}"
            new_v = (
                max(versions[new_k], versions.pop(k))
                if new_k in versions
                else versions.pop(k)
            )
            # 更新 seen
            for ss in (seen.get(node, {}), seen.get(INTERRUPT, {})):
                if k in ss:
                    s = ss.pop(k)
                    if new_k in ss:
                        ss[new_k] = max(s, ss[new_k])
                    else:
                        ss[new_k] = s
            # 更新值
            if new_k not in values and k in values:
                values[new_k] = values.pop(k)
            # 更新版本
            versions[new_k] = new_v

    # 迁移 2: branch:source:condition:node -&gt; branch:to:node
    for k in list(versions):
        if k.startswith("branch:") and k.count(":") == 3:
            node = k.split(":")[-1]
            if node not in self.nodes:
                continue
            new_k = f"branch:to:{node}"
            new_v = (
                max(versions[new_k], versions.pop(k))
                if new_k in versions
                else versions.pop(k)
            )
            # 更新 seen
            for ss in (seen.get(node, {}), seen.get(INTERRUPT, {})):
                if k in ss:
                    s = ss.pop(k)
                    if new_k in ss:
                        ss[new_k] = max(s, ss[new_k])
                    else:
                        ss[new_k] = s
            # 更新值
            if new_k not in values and k in values:
                values[new_k] = values.pop(k)
            # 更新版本
            versions[new_k] = new_v

    # 迁移 3: node -&gt; branch:to:node（对于所有目标节点）
    if not set(self.nodes).isdisjoint(versions):
        source_to_target = defaultdict(list)
        for start, end in self.builder.edges:
            if start != START and end != END:
                source_to_target[start].append(end)
        for k in list(versions):
            if k == START:
                continue
            if k in self.nodes:
                v = versions.pop(k)
                c = values.pop(k, MISSING)
                for end in source_to_target[k]:
                    new_k = f"branch:to:{end}"
                    new_v = max(versions[new_k], v) if new_k in versions else v
                    # 更新 seen
                    for ss in (seen.get(end, {}), seen.get(INTERRUPT, {})):
                        if k in ss:
                            s = ss.pop(k)
                            if new_k in ss:
                                ss[new_k] = max(s, ss[new_k])
                            else:
                                ss[new_k] = s
                    # 更新值
                    if new_k not in values and c is not MISSING:
                        values[new_k] = c
                    # 更新版本
                    versions[new_k] = new_v
                # 弹出中断 seen
                if INTERRUPT in seen:
                    seen[INTERRUPT].pop(k, MISSING)
</code></pre>
<hr />
<h2>辅助函数</h2>
<h3><code>_pick_mapper</code> 函数</h3>
<pre><code>def _pick_mapper(
    state_keys: Sequence[str], schema: type[Any]
) -&gt; Callable[[Any], Any] | None:
    """
    选择状态映射函数。
    
    如果状态键是 __root__ 或模式是字典类型，返回 None（不需要映射）。
    否则返回一个将字典转换为模式对象的函数。
    """
    if state_keys == ["__root__"]:
        return None
    if isclass(schema) and issubclass(schema, dict):
        return None
    return partial(_coerce_state, schema)
</code></pre>
<h3><code>_coerce_state</code> 函数</h3>
<pre><code>def _coerce_state(schema: type[Any], input: dict[str, Any]) -&gt; dict[str, Any]:
    """
    将字典强制转换为模式对象。
    
    使用模式类的构造函数创建实例。
    """
    return schema(**input)
</code></pre>
<h3><code>_control_branch</code> 函数</h3>
<pre><code>def _control_branch(value: Any) -&gt; Sequence[tuple[str, Any]]:
    """
    处理分支控制值。
    
    将 Command 对象或 Send 对象转换为写入条目。
    
    参数：
    - value: 控制值（Command、Send 或列表）
    
    返回：
    - 写入条目列表
    """
    if isinstance(value, Send):
        return ((TASKS, value),)
    
    commands: list[Command] = []
    if isinstance(value, Command):
        commands.append(value)
    elif isinstance(value, (list, tuple)):
        for cmd in value:
            if isinstance(cmd, Command):
                commands.append(cmd)
    
    rtn: list[tuple[str, Any]] = []
    for command in commands:
        # 如果是父命令，抛出异常
        if command.graph == Command.PARENT:
            raise ParentCommand(command)

        # 获取目标节点
        goto_targets = (
            [command.goto] if isinstance(command.goto, (Send, str)) else command.goto
        )

        # 转换为写入条目
        for go in goto_targets:
            if isinstance(go, Send):
                rtn.append((TASKS, go))
            elif isinstance(go, str) and go != END:
                rtn.append((CHANNEL_BRANCH_TO.format(go), None))
    return rtn
</code></pre>
<h3><code>_control_static</code> 函数</h3>
<pre><code>def _control_static(
    ends: tuple[str, ...] | dict[str, str],
) -&gt; Sequence[tuple[str, Any, str | None]]:
    """
    创建静态分支控制条目。
    
    用于节点规范中指定的目标节点。
    
    参数：
    - ends: 目标节点（元组或字典）
    
    返回：
    - 写入条目列表（包含标签）
    """
    if isinstance(ends, dict):
        return [
            (k if k == END else CHANNEL_BRANCH_TO.format(k), None, label)
            for k, label in ends.items()
        ]
    else:
        return [
            (e if e == END else CHANNEL_BRANCH_TO.format(e), None, None) for e in ends
        ]
</code></pre>
<h3><code>_get_root</code> 函数</h3>
<pre><code>def _get_root(input: Any) -&gt; Sequence[tuple[str, Any]] | None:
    """
    处理根值更新。
    
    当状态只有一个根键（__root__）时使用。
    
    参数：
    - input: 输入值（Command、列表或普通值）
    
    返回：
    - 更新元组列表
    """
    if isinstance(input, Command):
        if input.graph == Command.PARENT:
            return ()
        return input._update_as_tuples()
    elif (
        isinstance(input, (list, tuple))
        and input
        and any(isinstance(i, Command) for i in input)
    ):
        updates: list[tuple[str, Any]] = []
        for i in input:
            if isinstance(i, Command):
                if i.graph == Command.PARENT:
                    continue
                updates.extend(i._update_as_tuples())
            else:
                updates.append(("__root__", i))
        return updates
    elif input is not None:
        return [("__root__", input)]
</code></pre>
<h3><code>_get_channels</code> 函数</h3>
<pre><code>def _get_channels(
    schema: type[dict],
) -&gt; tuple[dict[str, BaseChannel], dict[str, ManagedValueSpec], dict[str, Any]]:
    """
    从模式中提取通道和托管值。
    
    参数：
    - schema: 模式类型
    
    返回：
    - (通道字典, 托管值字典, 类型提示字典)
    """
    
    &gt; 💡 **通俗理解**：这个方法就像"解析表格模板，为每个字段创建邮箱"。
    &gt; - 输入：一个状态模式（定义了有哪些字段）
    &gt; - 输出：为每个字段创建的通道（邮箱）和托管值（特殊邮箱）
    &gt; - 就像你定义了一个表格，系统自动为每列创建一个"数据传递通道"
    
    # 如果没有注解，返回根通道
    if not hasattr(schema, "__annotations__"):
        return (
            {"__root__": _get_channel("__root__", schema, allow_managed=False)},
            {},
            {},
        )

    # 获取类型提示（包括额外信息）
    type_hints = get_type_hints(schema, include_extras=True)
    
    # 为每个字段创建通道或托管值
    all_keys = {
        name: _get_channel(name, typ)
        for name, typ in type_hints.items()
        if name != "__slots__"
    }
    
    # 分离通道和托管值
    return (
        {k: v for k, v in all_keys.items() if isinstance(v, BaseChannel)},
        {k: v for k, v in all_keys.items() if is_managed_value(v)},
        type_hints,
    )
</code></pre>
<h3><code>_get_channel</code> 函数</h3>
<pre><code>@overload
def _get_channel(
    name: str, annotation: Any, *, allow_managed: Literal[False]
) -&gt; BaseChannel: ...

@overload
def _get_channel(
    name: str, annotation: Any, *, allow_managed: Literal[True] = True
) -&gt; BaseChannel | ManagedValueSpec: ...

def _get_channel(
    name: str, annotation: Any, *, allow_managed: bool = True
) -&gt; BaseChannel | ManagedValueSpec:
    """
    从类型注解创建通道或托管值。
    
    检查顺序：
    1. 是否为托管值
    2. 是否为显式通道
    3. 是否为二元操作符聚合
    4. 默认使用 LastValue
    
    参数：
    - name: 字段名
    - annotation: 类型注解
    - allow_managed: 是否允许托管值
    
    返回：
    - 通道或托管值规范
    """
    
    &gt; 💡 **通俗理解**：这个方法就像"根据字段类型选择合适的邮箱类型"。
    &gt; - 如果字段标注了"托管值"（系统自动管理），就用托管值通道
    &gt; - 如果字段标注了特定通道类型，就用那个类型
    &gt; - 如果字段有 reducer（聚合函数），就用聚合通道（可以合并多个值）
    &gt; - 否则默认用 LastValue（只保留最新值，覆盖旧值）
    &gt; 就像根据不同的需求选择不同的"邮箱类型"：有的邮箱只存最新邮件，有的可以合并多封邮件。
    
    # 检查托管值
    if manager := _is_field_managed_value(name, annotation):
        if allow_managed:
            return manager
        else:
            raise ValueError(f"This {annotation} not allowed in this position")
    
    # 检查显式通道
    elif channel := _is_field_channel(annotation):
        channel.key = name
        return channel
    
    # 检查二元操作符聚合
    elif channel := _is_field_binop(annotation):
        channel.key = name
        return channel

    # 默认使用 LastValue
    fallback: LastValue = LastValue(annotation)
    fallback.key = name
    return fallback
</code></pre>
<h3><code>_is_field_channel</code> 函数</h3>
<pre><code>def _is_field_channel(typ: type[Any]) -&gt; BaseChannel | None:
    """
    检查类型注解是否为显式通道。
    
    检查注解的元数据中是否包含 BaseChannel 实例或类。
    """
    if hasattr(typ, "__metadata__"):
        meta = typ.__metadata__
        if len(meta) &gt;= 1 and isinstance(meta[-1], BaseChannel):
            return meta[-1]
        elif len(meta) &gt;= 1 and isclass(meta[-1]) and issubclass(meta[-1], BaseChannel):
            return meta[-1](typ.__origin__ if hasattr(typ, "__origin__") else typ)
    return None
</code></pre>
<h3><code>_is_field_binop</code> 函数</h3>
<pre><code>def _is_field_binop(typ: type[Any]) -&gt; BinaryOperatorAggregate | None:
    """
    检查类型注解是否为二元操作符聚合（reducer）。
    
    检查注解的元数据中是否包含可调用对象，且签名符合 reducer 要求（两个位置参数）。
    """
    if hasattr(typ, "__metadata__"):
        meta = typ.__metadata__
        if len(meta) &gt;= 1 and callable(meta[-1]):
            sig = signature(meta[-1])
            params = list(sig.parameters.values())
            if (
                sum(
                    p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD)
                    for p in params
                )
                == 2
            ):
                return BinaryOperatorAggregate(typ, meta[-1])
            else:
                raise ValueError(
                    f"Invalid reducer signature. Expected (a, b) -&gt; c. Got {sig}"
                )
    return None
</code></pre>
<h3><code>_is_field_managed_value</code> 函数</h3>
<pre><code>def _is_field_managed_value(name: str, typ: type[Any]) -&gt; ManagedValueSpec | None:
    """
    检查类型注解是否为托管值。
    
    检查注解的元数据中是否包含托管值装饰器。
    """
    if hasattr(typ, "__metadata__"):
        meta = typ.__metadata__
        if len(meta) &gt;= 1:
            decoration = get_origin(meta[-1]) or meta[-1]
            if is_managed_value(decoration):
                return decoration

    return None
</code></pre>
<h3><code>_get_json_schema</code> 函数</h3>
<pre><code>def _get_json_schema(
    typ: type,
    schemas: dict,
    channels: dict,
    name: str,
) -&gt; dict[str, Any]:
    """
    生成类型的 JSON Schema。
    
    用于 API 文档和验证。
    
    参数：
    - typ: 类型
    - schemas: 模式字典
    - channels: 通道字典
    - name: Schema 名称
    
    返回：
    - JSON Schema 字典
    """
    # Pydantic 模型
    if isclass(typ) and issubclass(typ, BaseModel):
        return typ.model_json_schema()
    
    # TypedDict
    elif is_typeddict(typ):
        return TypeAdapter(typ).json_schema()
    
    # 其他类型
    else:
        keys = list(schemas[typ].keys())
        
        # 单根键
        if len(keys) == 1 and keys[0] == "__root__":
            return create_model(
                name,
                root=(channels[keys[0]].UpdateType, None),
            ).model_json_schema()
        
        # 多键
        else:
            return create_model(
                name,
                field_definitions={
                    k: (
                        channels[k].UpdateType,
                        (
                            get_field_default(
                                k,
                                channels[k].UpdateType,
                                typ,
                            )
                        ),
                    )
                    for k in schemas[typ]
                    if k in channels and isinstance(channels[k], BaseChannel)
                },
            ).model_json_schema()
</code></pre>
<h3>常量</h3>
<pre><code>CHANNEL_BRANCH_TO = "branch:to:{}"
# 分支通道名称格式模板
# 用于创建节点分支通道的名称，例如 "branch:to:node_name"
</code></pre>
<hr />
<h2>总结</h2>
<p><code>state.py</code> 文件实现了 LangGraph 的核心状态图功能：</p>
<ol>
<li><strong>StateGraph 类</strong>：用于构建状态图，支持节点、边、条件边等</li>
<li><strong>CompiledStateGraph 类</strong>：编译后的图，实现实际执行逻辑</li>
<li><strong>通道系统</strong>：通过通道实现节点间的状态共享</li>
<li><strong>模式系统</strong>：支持 TypedDict、Pydantic 模型等多种状态模式</li>
<li><strong>检查点迁移</strong>：支持旧版本检查点的自动迁移</li>
</ol>
<p>关键概念：</p>
<ul>
<li><strong>节点（Node）</strong>：图的执行单元，接收状态并返回部分状态更新</li>
<li><strong>边（Edge）</strong>：定义节点间的执行顺序</li>
<li><strong>条件边（Conditional Edge）</strong>：根据状态动态选择下一个节点</li>
<li><strong>通道（Channel）</strong>：节点间共享状态的机制</li>
<li><strong>模式（Schema）</strong>：定义状态的结构和类型</li>
</ul>
<p>这个文件是 LangGraph 框架的核心，理解它有助于深入理解整个框架的工作原理。</p>
<hr />
<h3>💡 整体理解：用生活化的比喻</h3>
<p><strong>StateGraph（设计阶段）</strong>：</p>
<ul>
<li>就像画流程图，你定义有哪些步骤（节点）、步骤之间的顺序（边）、根据情况选择路径（条件边）</li>
<li>你还可以设置每个步骤的配置：失败后重试几次、结果是否缓存、执行完后可以去哪里</li>
</ul>
<p><strong>CompiledStateGraph（运行阶段）</strong>：</p>
<ul>
<li>就像把流程图转换成实际的工作流程系统</li>
<li>每个节点都有一个"邮箱"（通道）来传递数据</li>
<li>系统会自动调度：哪个节点先执行、什么时候执行、如何传递数据</li>
</ul>
<p><strong>通道系统</strong>：</p>
<ul>
<li>就像公司里的"共享文件夹"或"公告板"</li>
<li>每个状态字段都有自己的"文件夹"（通道）</li>
<li>节点可以读取和写入这些文件夹</li>
<li>不同类型的通道有不同的行为：
<ul>
<li><code>LastValue</code>：只保留最新值（覆盖旧值）</li>
<li><code>NamedBarrierValue</code>：等待多个节点都完成（集合点）</li>
<li><code>BinaryOperatorAggregate</code>：用函数合并多个值（如累加、列表合并）</li>
</ul>
</li>
</ul>
<p><strong>执行流程</strong>：</p>
<ol>
<li>你调用 <code>graph.invoke(input)</code> 或 <code>graph.stream(input)</code></li>
<li>系统从 START 节点开始</li>
<li>每个节点执行时：
<ul>
<li>从通道读取需要的状态字段</li>
<li>执行节点函数</li>
<li>把返回值写入通道（更新状态）</li>
<li>根据边或条件边决定下一个节点</li>
</ul>
</li>
<li>重复步骤 3，直到到达 END 节点或没有更多节点可执行</li>
<li>返回最终状态</li>
</ol>
<p><strong>检查点系统</strong>：</p>
<ul>
<li>就像游戏存档，可以在任意时刻保存当前状态</li>
<li>如果出错了，可以从检查点恢复，不用从头开始</li>
<li>支持版本迁移：即使 LangGraph 内部实现改变了，旧存档也能用</li>
</ul>
<hr />
<h3>🎯 关键理解点</h3>
<ol>
<li><strong>状态是共享的</strong>：所有节点共享同一个状态对象，通过通道来读写</li>
<li><strong>节点只返回部分更新</strong>：节点不需要返回完整状态，只返回它要更新的字段</li>
<li><strong>编译是必须的</strong>：<code>StateGraph</code> 只是设计，必须调用 <code>compile()</code> 才能运行</li>
<li><strong>通道是核心</strong>：节点间的数据传递都通过通道，通道类型决定了数据如何合并</li>
<li><strong>类型提示很重要</strong>：LangGraph 会从类型提示推断很多信息，写好类型提示可以让代码更清晰</li>
</ol>
<p>希望这些解释能帮助你更好地理解 LangGraph 的工作原理！🚀</p>
]]></content>
    <author><name>Hygen</name></author>
    <category term="Guides"/>
  </entry>
</feed>
