<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="zh_CN">
  <title>Home-LxuMzn</title>
  <subtitle>No description</subtitle>
  <link href="https://lyuumine.cn/" rel="alternate" type="text/html"/>
  <link href="https://lyuumine.cn/atom.xml" rel="self" type="application/atom+xml"/>
  <id>https://lyuumine.cn/</id>
  <updated>2026-10-13T00:00:00.000Z</updated>
  <entry>
    <title>表达式求值与二叉树遍历教程</title>
    <link href="https://lyuumine.cn/posts/expression-evaluation-and-binary-tree/" rel="alternate" type="text/html"/>
    <id>https://lyuumine.cn/posts/expression-evaluation-and-binary-tree/</id>
    <published>2026-10-13T00:00:00.000Z</published>
    <updated>2026-10-13T00:00:00.000Z</updated>
    <summary>从零讲清中缀、前缀、后缀表达式与二叉树三种遍历的对应关系，含双栈法、调度场算法与表达式求值实现。</summary>
    <content type="html"><![CDATA[<p>本文从零讲解中缀、前缀、后缀三种表达式，以及它们与二叉树三种遍历的对应关系，并给出双栈法、调度场算法、后缀/前缀求值等经典算法。全文用一个例子贯穿：</p>
<pre><code>中缀：3 + 4 * 2 / (1 - 5)^2
后缀：3 4 2 * 1 5 - 2 ^ / +
前缀：+ 3 / * 4 2 ^ - 1 5 2
</code></pre>
<p>最终结果都是 <code>3.5</code>。</p>
<hr />
<h2>1. 三种表达式是什么</h2>
<h3>1.1 中缀表达式</h3>
<p>人最熟悉的写法，运算符在两个操作数中间：</p>
<pre><code>3 + 4 * 2
</code></pre>
<p>特点：有优先级、有括号，计算机直接求值麻烦。</p>
<h3>1.2 后缀表达式（逆波兰式 RPN）</h3>
<p>运算符在操作数后面：</p>
<pre><code>3 4 2 * +
</code></pre>
<p>特点：没有括号，运算顺序唯一，计算机最容易求值。</p>
<h3>1.3 前缀表达式（波兰式 PN）</h3>
<p>运算符在操作数前面：</p>
<pre><code>+ 3 * 4 2
</code></pre>
<p>特点：同样没有括号，求值时从右往左扫描即可。</p>
<hr />
<h2>2. 表达式树：三种表达式的统一来源</h2>
<p>任何中缀表达式都可以画成一棵二叉树：</p>
<ul>
<li>叶子节点：数字</li>
<li>内部节点：运算符</li>
<li>左子树、右子树：参与运算的子表达式</li>
</ul>
<p>以 <code>3 + 4 * 2 / (1 - 5)^2</code> 为例，表达式树为：</p>
<pre><code>            +
          /   \
         3     /
              / \
             *   ^
            / \ / \
           4  2 -   2
               / \
              1   5
</code></pre>
<h3>2.1 三种遍历对应三种表达式</h3>
<table>
<thead>
<tr>
<th>遍历方式</th>
<th>顺序</th>
<th>得到的表达式</th>
<th>例子</th>
</tr>
</thead>
<tbody>
<tr>
<td>前序遍历</td>
<td>根 → 左 → 右</td>
<td>前缀表达式 PN</td>
<td><code>+ 3 / * 4 2 ^ - 1 5 2</code></td>
</tr>
<tr>
<td>中序遍历</td>
<td>左 → 根 → 右</td>
<td>中缀表达式（需补括号）</td>
<td><code>3 + 4 * 2 / (1 - 5)^2</code></td>
</tr>
<tr>
<td>后序遍历</td>
<td>左 → 右 → 根</td>
<td>后缀表达式 RPN</td>
<td><code>3 4 2 * 1 5 - 2 ^ / +</code></td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong>一句话总结</strong>：同一棵表达式树，三种遍历顺序，得到三种表达式。</p>
</blockquote>
<hr />
<h2>3. 中缀表达式求值：双栈法</h2>
<p>双栈法直接扫描中缀表达式，一边读一边算。</p>
<h3>3.1 规则</h3>
<ul>
<li>一个数字栈 <code>num</code>，一个运算符栈 <code>op</code></li>
<li>从左到右扫描</li>
<li>遇到数字 → 压 <code>num</code></li>
<li>遇到左括号 → 压 <code>op</code></li>
<li>遇到右括号 → 弹 <code>op</code> 直到左括号，每弹一个运算符就算一次</li>
<li>遇到运算符 → 比较优先级：
<ul>
<li>栈顶优先级 <strong>高于</strong> 当前，或 <strong>同级且当前不是 <code>^</code></strong>，就弹栈顶计算</li>
<li>否则当前运算符入 <code>op</code></li>
</ul>
</li>
<li>扫描完，把 <code>op</code> 里剩下的全部弹出算完</li>
</ul>
<h3>3.2 模拟 <code>3 + 4 * 2 / (1 - 5)^2</code></h3>
<table>
<thead>
<tr>
<th>步骤</th>
<th>读到</th>
<th>动作</th>
<th>num 栈</th>
<th>op 栈</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td><code>3</code></td>
<td>数字入栈</td>
<td><code>[3]</code></td>
<td><code>[]</code></td>
</tr>
<tr>
<td>2</td>
<td><code>+</code></td>
<td>入栈</td>
<td><code>[3]</code></td>
<td><code>[+]</code></td>
</tr>
<tr>
<td>3</td>
<td><code>4</code></td>
<td>数字入栈</td>
<td><code>[3,4]</code></td>
<td><code>[+]</code></td>
</tr>
<tr>
<td>4</td>
<td><code>*</code></td>
<td><code>+</code> 优先级低，不弹；<code>*</code> 入栈</td>
<td><code>[3,4]</code></td>
<td><code>[+,*]</code></td>
</tr>
<tr>
<td>5</td>
<td><code>2</code></td>
<td>数字入栈</td>
<td><code>[3,4,2]</code></td>
<td><code>[+,*]</code></td>
</tr>
<tr>
<td>6</td>
<td><code>/</code></td>
<td>弹 <code>*</code> 算 <code>4*2=8</code>；<code>+</code> 低不弹；<code>/</code> 入栈</td>
<td><code>[3,8]</code></td>
<td><code>[+,/]</code></td>
</tr>
<tr>
<td>7</td>
<td><code>(</code></td>
<td>入栈</td>
<td><code>[3,8]</code></td>
<td><code>[+,/,(]</code></td>
</tr>
<tr>
<td>8</td>
<td><code>1</code></td>
<td>数字入栈</td>
<td><code>[3,8,1]</code></td>
<td><code>[+,/,(]</code></td>
</tr>
<tr>
<td>9</td>
<td><code>-</code></td>
<td>栈顶是 <code>(</code>，不弹；<code>-</code> 入栈</td>
<td><code>[3,8,1]</code></td>
<td><code>[+,/,(,-]</code></td>
</tr>
<tr>
<td>10</td>
<td><code>5</code></td>
<td>数字入栈</td>
<td><code>[3,8,1,5]</code></td>
<td><code>[+,/,(,-]</code></td>
</tr>
<tr>
<td>11</td>
<td><code>)</code></td>
<td>弹 <code>-</code> 算 <code>1-5=-4</code>；遇 <code>(</code> 弹掉</td>
<td><code>[3,8,-4]</code></td>
<td><code>[+,/]</code></td>
</tr>
<tr>
<td>12</td>
<td><code>^</code></td>
<td><code>/</code> 优先级低，不弹；<code>^</code> 入栈</td>
<td><code>[3,8,-4]</code></td>
<td><code>[+,/,^]</code></td>
</tr>
<tr>
<td>13</td>
<td><code>2</code></td>
<td>数字入栈</td>
<td><code>[3,8,-4,2]</code></td>
<td><code>[+,/,^]</code></td>
</tr>
<tr>
<td>14</td>
<td>结束</td>
<td>弹 <code>^</code> 算 <code>(-4)^2=16</code></td>
<td><code>[3,8,16]</code></td>
<td><code>[+,/]</code></td>
</tr>
<tr>
<td>15</td>
<td>结束</td>
<td>弹 <code>/</code> 算 <code>8/16=0.5</code></td>
<td><code>[3,0.5]</code></td>
<td><code>[+]</code></td>
</tr>
<tr>
<td>16</td>
<td>结束</td>
<td>弹 <code>+</code> 算 <code>3+0.5=3.5</code></td>
<td><code>[3.5]</code></td>
<td><code>[]</code></td>
</tr>
</tbody>
</table>
<p>结果 <code>3.5</code>。</p>
<h3>3.3 双栈法与后序遍历的关系</h3>
<p>双栈法每次弹栈计算，相当于在表达式树中合并一棵小子树：</p>
<ul>
<li><code>num</code> 栈存的是已经算完的子树结果</li>
<li><code>op</code> 栈存的是还没轮到算的子树根</li>
<li>弹出一个运算符，就表示它的左右子树都已完成，可以访问这个根</li>
</ul>
<p>所以：</p>
<blockquote>
<p><strong>双栈法 = 一边隐式建表达式树，一边做后序遍历求值。</strong></p>
</blockquote>
<hr />
<h2>4. 中缀转后缀：调度场算法</h2>
<p>调度场算法由 Dijkstra 提出，专门用来把中缀转成后缀。</p>
<h3>4.1 规则</h3>
<p>准备一个输出队列和一个运算符栈：</p>
<ul>
<li>遇到数字 → 加入输出队列</li>
<li>遇到左括号 → 压入运算符栈</li>
<li>遇到右括号 → 不断弹出运算符栈顶加入输出队列，直到遇到左括号，然后把左括号弹掉</li>
<li>遇到运算符 →
<ul>
<li>只要栈不空，且栈顶不是左括号，且栈顶优先级 <strong>高于</strong> 当前，或 <strong>同级且当前是左结合</strong>，就弹出栈顶加入输出</li>
<li>然后把当前运算符压入栈</li>
</ul>
</li>
<li>扫描结束 → 把运算符栈里剩下的全部弹出加入输出</li>
</ul>
<h3>4.2 模拟 <code>3 + 4 * 2 / (1 - 5)^2</code></h3>
<table>
<thead>
<tr>
<th>步骤</th>
<th>读到</th>
<th>动作</th>
<th>输出队列</th>
<th>运算符栈</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td><code>3</code></td>
<td>数字输出</td>
<td><code>3</code></td>
<td><code>[]</code></td>
</tr>
<tr>
<td>2</td>
<td><code>+</code></td>
<td>压栈</td>
<td><code>3</code></td>
<td><code>[+]</code></td>
</tr>
<tr>
<td>3</td>
<td><code>4</code></td>
<td>数字输出</td>
<td><code>3 4</code></td>
<td><code>[+]</code></td>
</tr>
<tr>
<td>4</td>
<td><code>*</code></td>
<td><code>+</code> 低，不弹；压 <code>*</code></td>
<td><code>3 4</code></td>
<td><code>[+,*]</code></td>
</tr>
<tr>
<td>5</td>
<td><code>2</code></td>
<td>数字输出</td>
<td><code>3 4 2</code></td>
<td><code>[+,*]</code></td>
</tr>
<tr>
<td>6</td>
<td><code>/</code></td>
<td>弹 <code>*</code> 输出；<code>+</code> 低不弹；压 <code>/</code></td>
<td><code>3 4 2 *</code></td>
<td><code>[+,/]</code></td>
</tr>
<tr>
<td>7</td>
<td><code>(</code></td>
<td>压栈</td>
<td><code>3 4 2 *</code></td>
<td><code>[+,/,(]</code></td>
</tr>
<tr>
<td>8</td>
<td><code>1</code></td>
<td>数字输出</td>
<td><code>3 4 2 * 1</code></td>
<td><code>[+,/,(]</code></td>
</tr>
<tr>
<td>9</td>
<td><code>-</code></td>
<td>栈顶是 <code>(</code>，不弹；压 <code>-</code></td>
<td><code>3 4 2 * 1</code></td>
<td><code>[+,/,(,-]</code></td>
</tr>
<tr>
<td>10</td>
<td><code>5</code></td>
<td>数字输出</td>
<td><code>3 4 2 * 1 5</code></td>
<td><code>[+,/,(,-]</code></td>
</tr>
<tr>
<td>11</td>
<td><code>)</code></td>
<td>弹 <code>-</code> 输出；弹掉 <code>(</code></td>
<td><code>3 4 2 * 1 5 -</code></td>
<td><code>[+,/]</code></td>
</tr>
<tr>
<td>12</td>
<td><code>^</code></td>
<td><code>/</code> 低，不弹；压 <code>^</code></td>
<td><code>3 4 2 * 1 5 -</code></td>
<td><code>[+,/,^]</code></td>
</tr>
<tr>
<td>13</td>
<td><code>2</code></td>
<td>数字输出</td>
<td><code>3 4 2 * 1 5 - 2</code></td>
<td><code>[+,/,^]</code></td>
</tr>
<tr>
<td>14</td>
<td>结束</td>
<td>弹 <code>^</code>、<code>/</code>、<code>+</code></td>
<td><code>3 4 2 * 1 5 - 2 ^ / +</code></td>
<td><code>[]</code></td>
</tr>
</tbody>
</table>
<p>得到后缀表达式：</p>
<pre><code>3 4 2 * 1 5 - 2 ^ / +
</code></pre>
<hr />
<h2>5. 后缀表达式求值</h2>
<p>后缀求值只需要一个数字栈。</p>
<h3>5.1 规则</h3>
<ul>
<li>从左到右扫描</li>
<li>遇到数字 → 压栈</li>
<li>遇到运算符 → 弹出两个数：
<ul>
<li>先弹出的是 <strong>右操作数 <code>b</code></strong></li>
<li>后弹出的是 <strong>左操作数 <code>a</code></strong></li>
<li>计算 <code>a op b</code>，结果压回栈</li>
</ul>
</li>
<li>最后栈里只剩一个数，就是结果</li>
</ul>
<h3>5.2 模拟 <code>3 4 2 * 1 5 - 2 ^ / +</code></h3>
<table>
<thead>
<tr>
<th>步骤</th>
<th>读到</th>
<th>动作</th>
<th>栈</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td><code>3</code></td>
<td>压栈</td>
<td><code>[3]</code></td>
</tr>
<tr>
<td>2</td>
<td><code>4</code></td>
<td>压栈</td>
<td><code>[3,4]</code></td>
</tr>
<tr>
<td>3</td>
<td><code>2</code></td>
<td>压栈</td>
<td><code>[3,4,2]</code></td>
</tr>
<tr>
<td>4</td>
<td><code>*</code></td>
<td>弹 <code>b=2, a=4</code>，算 <code>4*2=8</code></td>
<td><code>[3,8]</code></td>
</tr>
<tr>
<td>5</td>
<td><code>1</code></td>
<td>压栈</td>
<td><code>[3,8,1]</code></td>
</tr>
<tr>
<td>6</td>
<td><code>5</code></td>
<td>压栈</td>
<td><code>[3,8,1,5]</code></td>
</tr>
<tr>
<td>7</td>
<td><code>-</code></td>
<td>弹 <code>b=5, a=1</code>，算 <code>1-5=-4</code></td>
<td><code>[3,8,-4]</code></td>
</tr>
<tr>
<td>8</td>
<td><code>2</code></td>
<td>压栈</td>
<td><code>[3,8,-4,2]</code></td>
</tr>
<tr>
<td>9</td>
<td><code>^</code></td>
<td>弹 <code>b=2, a=-4</code>，算 <code>(-4)^2=16</code></td>
<td><code>[3,8,16]</code></td>
</tr>
<tr>
<td>10</td>
<td><code>/</code></td>
<td>弹 <code>b=16, a=8</code>，算 <code>8/16=0.5</code></td>
<td><code>[3,0.5]</code></td>
</tr>
<tr>
<td>11</td>
<td><code>+</code></td>
<td>弹 <code>b=0.5, a=3</code>，算 <code>3+0.5=3.5</code></td>
<td><code>[3.5]</code></td>
</tr>
</tbody>
</table>
<p>结果 <code>3.5</code>。</p>
<hr />
<h2>6. 前缀表达式求值</h2>
<p>前缀求值同样一个数字栈，但 <strong>从右往左扫描</strong>。</p>
<h3>6.1 规则</h3>
<ul>
<li>从右到左扫描</li>
<li>遇到数字 → 压栈</li>
<li>遇到运算符 → 弹出两个数：
<ul>
<li>先弹出的是 <strong>左操作数 <code>a</code></strong></li>
<li>后弹出的是 <strong>右操作数 <code>b</code></strong></li>
<li>计算 <code>a op b</code>，结果压回栈</li>
</ul>
</li>
<li>最后栈里只剩一个数，就是结果</li>
</ul>
<h3>6.2 模拟 <code>+ 3 / * 4 2 ^ - 1 5 2</code></h3>
<p>从右往左读的顺序是：</p>
<pre><code>2 5 1 - ^ 2 4 * / 3 +
</code></pre>
<table>
<thead>
<tr>
<th>步骤</th>
<th>读到</th>
<th>动作</th>
<th>栈</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td><code>2</code></td>
<td>压栈</td>
<td><code>[2]</code></td>
</tr>
<tr>
<td>2</td>
<td><code>5</code></td>
<td>压栈</td>
<td><code>[2,5]</code></td>
</tr>
<tr>
<td>3</td>
<td><code>1</code></td>
<td>压栈</td>
<td><code>[2,5,1]</code></td>
</tr>
<tr>
<td>4</td>
<td><code>-</code></td>
<td>弹 <code>a=1, b=5</code>，算 <code>1-5=-4</code></td>
<td><code>[2,-4]</code></td>
</tr>
<tr>
<td>5</td>
<td><code>^</code></td>
<td>弹 <code>a=-4, b=2</code>，算 <code>(-4)^2=16</code></td>
<td><code>[16]</code></td>
</tr>
<tr>
<td>6</td>
<td><code>2</code></td>
<td>压栈</td>
<td><code>[16,2]</code></td>
</tr>
<tr>
<td>7</td>
<td><code>4</code></td>
<td>压栈</td>
<td><code>[16,2,4]</code></td>
</tr>
<tr>
<td>8</td>
<td><code>*</code></td>
<td>弹 <code>a=4, b=2</code>，算 <code>4*2=8</code></td>
<td><code>[16,8]</code></td>
</tr>
<tr>
<td>9</td>
<td><code>/</code></td>
<td>弹 <code>a=8, b=16</code>，算 <code>8/16=0.5</code></td>
<td><code>[0.5]</code></td>
</tr>
<tr>
<td>10</td>
<td><code>3</code></td>
<td>压栈</td>
<td><code>[0.5,3]</code></td>
</tr>
<tr>
<td>11</td>
<td><code>+</code></td>
<td>弹 <code>a=3, b=0.5</code>，算 <code>3+0.5=3.5</code></td>
<td><code>[3.5]</code></td>
</tr>
</tbody>
</table>
<p>结果 <code>3.5</code>。</p>
<hr />
<h2>7. 三种求值方法对比</h2>
<table>
<thead>
<tr>
<th>表达式</th>
<th>扫描方向</th>
<th>栈的数量</th>
<th>弹栈顺序</th>
<th>需要优先级</th>
<th>需要括号</th>
</tr>
</thead>
<tbody>
<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>
<p><strong>记忆口诀</strong>：</p>
<blockquote>
<p>中缀双栈比优先，后缀从左先弹右，前缀从右先弹左。</p>
</blockquote>
<hr />
<h2>8. 递归下降求值（扩展）</h2>
<p>递归下降是编译器前端常用的方法，按文法递归解析。</p>
<p>文法示例：</p>
<pre><code>expr   -&gt; term { (+ | -) term }
term   -&gt; factor { (* | /) factor }
factor -&gt; number | '(' expr ')' | '-' factor
</code></pre>
<p>代码框架：</p>
<pre><code>double parseExpr(string&amp; s) {
    double val = parseTerm(s);
    while (pos &lt; s.size() &amp;&amp; (s[pos] == '+' || s[pos] == '-')) {
        char op = s[pos++];
        double rhs = parseTerm(s);
        if (op == '+') val += rhs;
        else val -= rhs;
    }
    return val;
}

double parseTerm(string&amp; s) {
    double val = parseFactor(s);
    while (pos &lt; s.size() &amp;&amp; (s[pos] == '*' || s[pos] == '/')) {
        char op = s[pos++];
        double rhs = parseFactor(s);
        if (op == '*') val *= rhs;
        else val /= rhs;
    }
    return val;
}

double parseFactor(string&amp; s) {
    if (s[pos] == '(') {
        pos++;
        double val = parseExpr(s);
        pos++;
        return val;
    }
    if (s[pos] == '-') {
        pos++;
        return -parseFactor(s);
    }
    double val = 0;
    while (pos &lt; s.size() &amp;&amp; isdigit(s[pos])) {
        val = val * 10 + (s[pos] - '0');
        pos++;
    }
    return val;
}
</code></pre>
<p>特点：结构清晰，易扩展，但需要处理左递归。</p>
<hr />
<h2>9. 总结</h2>
<ul>
<li><strong>中缀表达式</strong>：人熟悉，计算机麻烦，用双栈法求值。</li>
<li><strong>后缀表达式</strong>：一个栈，从左到右，先弹右后弹左。</li>
<li><strong>前缀表达式</strong>：一个栈，从右到左，先弹左后弹右。</li>
<li><strong>表达式树</strong>：三种遍历对应三种表达式。
<ul>
<li>前序 → 前缀</li>
<li>中序 → 中缀</li>
<li>后序 → 后缀</li>
</ul>
</li>
<li><strong>双栈法</strong>：本质是隐式建树 + 后序遍历求值。</li>
<li><strong>调度场算法</strong>：中缀转后缀的标准算法。</li>
</ul>
<hr />
<h2>10. 推荐练习</h2>
<ul>
<li><strong>408 真题</strong>：2014 年中缀转后缀、2025 年后缀求值入栈次数、括号匹配。</li>
<li><strong>LeetCode</strong>：150（逆波兰表达式求值）、224（基本计算器）、227（基本计算器 II）、772（基本计算器 III）、1597（构造二叉表达式树）。</li>
<li><strong>考研机试</strong>：上海交通大学 KY102、天津大学 2025 机试、浙大复试机试。</li>
<li><strong>参考书</strong>：王道《数据结构考研复习指导》第 3 章栈和队列；《算法竞赛入门经典》表达式求值章节；OI-wiki 表达式求值页面。</li>
</ul>
<hr />
<blockquote>
<p>本文可作为表达式求值专题的教程笔记，配合代码实践和手写模拟，效果更佳。</p>
</blockquote>
]]></content>
    <author><name>LxuMzn</name></author>
    <category term="数据结构与算法"/>
  </entry>
  <entry>
    <title>KMP 算法详解</title>
    <link href="https://lyuumine.cn/posts/kmp-algorithm/" rel="alternate" type="text/html"/>
    <id>https://lyuumine.cn/posts/kmp-algorithm/</id>
    <published>2026-10-13T00:00:00.000Z</published>
    <updated>2026-10-13T00:00:00.000Z</updated>
    <summary>从朴素匹配的缺陷讲起，推导 next 数组与 nextval 优化，附完整 C++ 实现与复杂度分析。</summary>
    <content type="html"><![CDATA[<h2>1. KMP 算法介绍</h2>
<p>KMP 算法：全称叫做「Knuth Morris Pratt 算法」，是由它的三位发明者 Donald Knuth、James H. Morris、Vaughan Pratt 的名字来命名的。KMP 算法是他们三人在 1977 年联合发表的。</p>
<ul>
<li>KMP 算法思想：对于给定文本串 <code>T</code> 与模式串 <code>p</code>，当发现文本串 <code>T</code> 的某个字符与模式串 <code>p</code> 不匹配的时候，可以利用匹配失败后的信息，尽量减少模式串与文本串的匹配次数，避免文本串位置的回退，以达到快速匹配的目的。</li>
</ul>
<h3>1.1 朴素匹配算法的缺陷</h3>
<p>在朴素匹配算法的匹配过程中，我们分别用指针 <code>i</code> 和指针 <code>j</code> 指示文本串 <code>T</code> 和模式串 <code>p</code> 中当前正在对比的字符。当发现文本串 <code>T</code> 的某个字符与模式串 <code>p</code> 不匹配的时候，<code>j</code> 回退到开始位置，<code>i</code> 回退到之前匹配开始位置的下一个位置上，然后开启新一轮的匹配，如图所示。</p>
<p><img src="image.png" alt="alt text" /></p>
<blockquote>
<p>图1-1：朴素匹配算法示意图（失配时 <code>i</code> 回退、<code>j</code> 归 0，从头重新匹配）</p>
</blockquote>
<p>这样，在 Brute Force 算法中，如果从文本串 <code>T[i]</code> 开始的这一趟字符串比较失败了，算法会直接开始尝试从 <code>T[i + 1]</code> 开始比较。如果 <code>i</code> 已经比较到了后边位置，则该操作相当于将指针 <code>i</code> 进行了回退操作。</p>
<p>那么有没有哪种算法，可以让 <code>i</code> 不发生回退，一直向右移动呢？</p>
<h3>1.2 KMP 算法的改进</h3>
<p>如果我们可以通过每一次的失配而得到一些「信息」，并且这些「信息」可以帮助我们跳过那些不可能匹配成功的位置，那么我们就能大大减少模式串与文本串的匹配次数，从而达到快速匹配的目的。</p>
<p>每一次失配所告诉我们的信息是：主串的某一个子串等于模式串的某一个前缀。</p>
<p>这个信息的意思是：如果文本串 <code>T[i: i + m]</code> 与模式串 <code>p</code> 的失配是下标位置 <code>j</code> 上发生的，那么文本串 <code>T</code> 从下标位置 <code>i</code> 开始连续的 <code>j - 1</code> 个字符，一定与模式串 <code>p</code> 的前 <code>j - 1</code> 个字符一模一样。</p>
<p>以刚才图中的例子来说，文本串的子串与模式串 <code>p</code> 的失配是在第 5 个位置发生的，那么：</p>
<p>文本串 <code>T</code> 从下标位置 <code>i</code> 开始连续的 5 个字符，一定与模式串 <code>p</code> 的前 5 个字符一模一样，即 <code>"ABCAB" == "ABCAB"</code>。</p>
<ul>
<li>而模式串的前 5 个字符中，前 2 位前缀和后 2 位后缀又是相同的，即 <code>"AB" == "AB"</code>。</li>
</ul>
<p>所以根据上面的信息，我们可以推出：文本串子串的后 2 位后缀和模式串子串的前 2 位是相同的，即这部分是之前已经比较过的，不需要再比较了，可以直接跳过。
<img src="image-1.png" alt="alt text" />
那么我们就可以将文本串中的失配字符直接对准模式串中对应前缀的下一个字符，继续进行对比。这样 <code>i</code> 就不再需要回退了，可以一直向右移动匹配下去。在这个过程中，我们只需要将模式串 <code>j</code> 进行回退操作即可。</p>
<blockquote>
<p>KMP 匹配算法</p>
</blockquote>
<p>KMP 算法就是使用了这样的思路，对模式串 <code>p</code> 进行了预处理，计算出一个「部分匹配表」，用一个数组 <code>next</code> 来记录。然后在每次失配发生时，不回退文本串的指针 <code>i</code>，而是根据「部分匹配表」来决定模式串可以向右移动的位数。</p>
<h2>2. 前缀与后缀</h2>
<p>在正式讲解 <code>next</code> 数组之前，需要先清楚「前缀」与「后缀」的定义。</p>
<ul>
<li><strong>真前缀（前缀）</strong>：从第 0 位开始、到任意位置结束的子串，不包括最后一个字符。</li>
<li><strong>真后缀（后缀）</strong>：从任意位置开始、到最后一位结束的子串，不包括第一个字符。</li>
<li><strong>最长公共前后缀</strong>：前缀集合 $\cap$ 后缀集合中最长的那一个。</li>
</ul>
<p>举个例子：对字符串 <code>"aabaa"</code> 来说：</p>
<ul>
<li>前缀集合：<code>{a, aa, aab, aaba}</code></li>
<li>后缀集合：<code>{a, aa, baa, abaa}</code></li>
<li>公共前后缀：<code>{a, aa}</code>，最长公共前后缀是 <code>"aa"</code>，长度为 2。</li>
</ul>
<p>小练习（建议先自己手算再看答案）：</p>
<ul>
<li><code>"ababa"</code> → 最长公共前后缀 <code>"aba"</code>，长度 3。</li>
<li><code>"aaaaa"</code> → 最长公共前后缀 <code>"aaaa"</code>（真前缀/真后缀），长度 4。</li>
<li><code>"abcabcab"</code> → 最长公共前后缀 <code>"abcab"</code>，长度 5。</li>
</ul>
<h2>3. next 数组</h2>
<h3>3.1 概念与人为规定</h3>
<p><code>next[j]</code> 代表模式串 <code>0</code> 到 <code>j - 1</code> 这段子串的最长公共前后缀的长度。也就是说，<code>next[j]</code> 记录的是：模式串第 <code>j</code> 个字符之前的子串中，最长相等的前缀与后缀的长度。</p>
<p>人为规定：<code>next[0] = -1</code>（因为 0 之前没有子串），<code>next[1] = 0</code>（因为只有一个字符时，真前缀集合为空）。</p>
<p>注意：有些题目或教材（如严蔚敏版《数据结构》）里的 <code>next</code> 数组下标从 1 开始，此时人为规定 <code>next[1] = 0</code>。两种写法本质相同：从 1 开始的数组，就是从 0 开始的数组整体加 1（下标同步平移），见 3.2 的对照表。做题前必须看清题目用的是哪一种约定。</p>
<h3>3.2 两种 next 数组约定（重点）</h3>
<p>「从 0 开始」和「从 1 开始」是两种常见的 <code>next</code> 数组写法，它们的本质含义相同，只是下标起点和数值相差 1。下面讲清楚两者的区别。</p>
<table>
<thead>
<tr>
<th>项目</th>
<th>约定一：从 0 开始（本讲义采用）</th>
<th>约定二：从 1 开始</th>
</tr>
</thead>
<tbody>
<tr>
<td>数组下标起点</td>
<td>下标从 0 开始</td>
<td>下标从 1 开始</td>
</tr>
<tr>
<td>人为规定</td>
<td><code>next[0] = -1</code>, <code>next[1] = 0</code></td>
<td><code>next[1] = 0</code></td>
</tr>
<tr>
<td><code>next[j]</code> 含义</td>
<td>模式串 <code>[0, j-1]</code> 的最长相等前后缀长度</td>
<td>模式串前 <code>j-1</code> 个字符（<code>[0, j-2]</code>）的最长相等前后缀长度 + 1</td>
</tr>
<tr>
<td>失配时怎么跳</td>
<td><code>i2 = next[i2];</code> 若 <code>i2 == 0</code>，则换主串开头</td>
<td><code>j = next[j];</code> 若 <code>j == 1</code>，则换主串开头</td>
</tr>
<tr>
<td>与约定一的关系</td>
<td>—</td>
<td><code>next1[j] = next0[j-1] + 1 (j &gt;= 2)</code></td>
</tr>
</tbody>
</table>
<p>对照关系举例：<code>"abaabac"</code> 中，约定一的 <code>next0[5] = 2</code>，约定二的 <code>next1[6] = 3 = 2 + 1</code>。整体平移后，约定二的 <code>next1[j]</code> 从 0 开始的数组各元素 <code>+1</code> 即可得到，例如 <code>[-1, 0, 0, ...]</code> → <code>[0, 1, 1, ...]</code>。</p>
<p><code>next</code> 数组的长度等于模式串长度 <code>m</code>：匹配过程中失配时 <code>i2</code> 最大只到 <code>m - 1</code>，用到的 <code>next[i2]</code> 下标都在 <code>0..m-1</code> 范围内，因此不需要求 <code>next[m]</code>。</p>
<p>为什么会有这两种写法？约定一（从 0 开始）配合 0 下标数组直接用，写代码最自然；约定二（从 1 开始）常见于教材（如严蔚敏版《数据结构》）中用 1 下标表示的串。两者在匹配时失配跳转的公式不同（一个是 <code>i2 = next[i2]</code>，一个是 <code>j = next[j]</code>），但只要和对应的匹配代码配套使用，结果完全一致。</p>
<h3>3.3 手算 next 数组</h3>
<p>以模式串 <code>"abaabac"</code> 为例，逐位手算 <code>next</code> 数组（采用约定一，下标从 0 开始）：</p>
<table>
<thead>
<tr>
<th>j</th>
<th>0</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
<th>5</th>
<th>6</th>
<th>7</th>
</tr>
</thead>
<tbody>
<tr>
<td>字符</td>
<td>a</td>
<td>b</td>
<td>a</td>
<td>a</td>
<td>b</td>
<td>c</td>
<td>a</td>
<td>c</td>
</tr>
<tr>
<td>子串 <code>[0, j-1]</code></td>
<td>—</td>
<td>a</td>
<td>ab</td>
<td>aba</td>
<td>abaa</td>
<td>abaab</td>
<td>abaabc</td>
<td>abaabca</td>
</tr>
<tr>
<td>最长公共前后缀长度</td>
<td>-1</td>
<td>0</td>
<td>0</td>
<td>1</td>
<td>1</td>
<td>2</td>
<td>0</td>
<td>1</td>
</tr>
<tr>
<td><code>next[j]</code></td>
<td>-1</td>
<td>0</td>
<td>0</td>
<td>1</td>
<td>1</td>
<td>2</td>
<td>0</td>
<td>1</td>
</tr>
</tbody>
</table>
<p>手算方法：对每个 <code>j</code>，只看模式串的前 <code>j</code> 个字符（下标 <code>0</code> 到 <code>j - 1</code>），找到它们的最长公共前后缀长度即可。例如 <code>j = 5</code> 时子串为 <code>"abaab"</code>，前缀与后缀最长相等的是 <code>"ab"</code>，所以 <code>next[5] = 2</code>。其中 <code>next[0] = -1</code>、<code>next[1] = 0</code> 是人为规定：0 之前没有子串，而只有一个字符时其前缀集合为空。</p>
<h3>3.4 递推计算 next 数组</h3>
<p>手算理解含义，但代码要用递推。递推过程中用一个变量 <code>cn</code> 记录「当前前缀的长度信息」（即 <code>next[i-1]</code>），从 <code>i = 2</code> 开始填表。递推规则只有三条：</p>
<ul>
<li>① 相等分支：若 <code>ms[i-1] == ms[cn]</code>，则 <code>next[i] = cn + 1</code>，<code>i</code>、<code>cn</code> 同时前进（等价写法：<code>next[i++] = ++cn</code>）。</li>
<li>② 回退分支：若不等且 <code>cn &gt; 0</code>，则 <code>cn = next[cn]</code>（<code>cn</code> 往前跳，跳到更短的前缀继续比）。</li>
<li>③ 结束分支：若 <code>cn</code> 已经跳不动了（<code>cn == 0</code> 且仍不等），则 <code>next[i] = 0</code>，<code>i</code> 前进。</li>
</ul>
<pre><code>// 递推计算 next 数组（约定一：next[0] = -1，next[1] = 0）
int i = 2;   // 当前要填 next[i] 的位置
int cn = 0;  // 前缀长度信息，等价于 next[i-1]
while (i &lt; ms.length) {
    if (ms[i-1] == ms[cn]) {          // ① 相等：i 处信息 = cn + 1
        next[i] = cn + 1;
        i++;
        cn = next[i-1];               // 等价于 ++cn
    } else if (cn &gt; 0) {              // ② 不等且还能跳：cn 回退到 next[cn]
        cn = next[cn];
    } else {                          // ③ 跳不动：next[i] = 0
        next[i] = 0;
    }
}
</code></pre>
<p>回退的本质：当 <code>i-1</code> 处字符与 <code>cn</code> 处字符不等时，说明以当前前缀长度无法延伸，就退而求其次，去尝试更短的前缀——而“更短的候选”恰好就是 <code>next[cn]</code>（最长公共前后缀的前缀部分）。</p>
<p>为什么递推的复杂度是 $O(M)$？因为 <code>i</code> 最多前进 <code>M</code> 次，<code>cn</code> 最多回退 <code>M</code> 次，总体是常数倍的操作次数。</p>
<h2>4. 完整代码 (C++)</h2>
<p>下面的代码采用「从 0 开始」的 <code>next</code> 约定（<code>next[0] = -1</code>），和上面第 3 章的叙述完全对应。</p>
<pre><code>#include &lt;iostream&gt;
#include &lt;string&gt;
#include &lt;vector&gt;
using namespace std;

// 求模式串 ms 的 next 数组
// next[j] = 模式串 ms[0..j-1] 的最长公共前后缀长度
// 人为规定：next[0] = -1，next[1] = 0
vector&lt;int&gt; getNextArray(const string&amp; ms) {
    if (ms.size() &lt;= 1) return ms.empty() ? vector&lt;int&gt;() : vector&lt;int&gt;{-1};
    vector&lt;int&gt; next(ms.size());
    next[0] = -1;
    next[1] = 0;
    int i = 2;   // 当前要填 next[i] 的位置
    int cn = 0;  // 前缀长度信息，等价于 next[i-1]
    while (i &lt; (int)ms.size()) {
        if (ms[i-1] == ms[cn]) {          // ① i-1 处字符与 cn 处字符相等
            next[i++] = ++cn;             // 经典写法：next[i] = cn + 1，然后 i、cn 同时前进
        } else if (cn &gt; 0) {              // ② 不等，cn 回退到 next[cn]
            cn = next[cn];
        } else {                          // ③ cn 跳不动，next[i] = 0
            next[i++] = 0;
        }
    }
    return next;
}

// KMP 匹配：返回 m 在 s 中第一次出现的位置，找不到返回 -1
// 约定：s.length() &gt;= m.length()
int getIndexOf(const string&amp; s, const string&amp; m) {
    if (s.length() &lt;= m.length()) return -1;
    if (m.length() == 0) return 0; // 空串按常理返回 0
    vector&lt;int&gt; next = getNextArray(m);
    int i1 = 0, i2 = 0; // i1 指向主串，i2 指向模式串
    while (i1 &lt; (int)s.length() &amp;&amp; i2 &lt; (int)m.length()) {
        if (s[i1] == m[i2]) {         // ① 相等，同时前进
            i1++;
            i2++;
        } else if (i2 == 0) {         // ② 连开头都匹配不上，换主串开头
            i1++;
        } else {                      // ③ 失配，模式串前推（主串指针不回退）
            i2 = next[i2];
        }
    }
    return (i2 == (int)m.length()) ? i1 - i2 : -1;
}
</code></pre>
<p>测试用例及运行结果（已实际编译运行验证）：</p>
<table>
<thead>
<tr>
<th>调用</th>
<th>结果</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>getIndexOf("abbcfddddbdcdaddebc", "ABCABCD")</code></td>
<td>-1</td>
</tr>
<tr>
<td><code>getIndexOf("abbcfddddbdcdaddebc", "bcf")</code></td>
<td>2</td>
</tr>
<tr>
<td><code>getIndexOf("aaaa", "bba")</code></td>
<td>-1</td>
</tr>
</tbody>
</table>
<h2>5. 小结：时间复杂度</h2>
<p>KMP 的时间复杂度要分两个阶段看：</p>
<ul>
<li>预处理阶段：构造 <code>next</code> 数组。<code>i</code> 最多前进 <code>M</code> 次，<code>cn</code> 最多回退 <code>M</code> 次（<code>M</code> 是模式串长度），所以是 $O(M)$。</li>
<li>匹配阶段：主串指针 <code>i</code> 全程不后退、最多前进 <code>N</code> 次（<code>N</code> 是主串长度）；失配时模式串指针每次最多回退 <code>M</code> 次，所以匹配阶段是 $O(N)$。</li>
</ul>
<p>总复杂度：$O(M) + O(N) = O(N + M)$，远优于暴力的 $O(N \times M)$。</p>
<h2>6. nextval 数组优化</h2>
<p>标准 KMP 已经很快了，但还存在一种「多余的跳转」：失配时跳到 <code>next[j]</code> 的位置后，如果该位置的字符和失配时的字符一样，那就还会再失配一次，白白浪费一次跳转。<code>nextval</code> 数组就是为了消除这种多余跳转而生的。</p>
<h3>6.1 优化思路：找出「跳过去还是失配」的坑</h3>
<p>先看一个具体例子：模式串 $p =$ <code>"aaaabab"</code>，主串为 <code>"aaaacaaaab"</code>。匹配到主串第 4 个字符 <code>c</code> 时失配，此时 $j = 4$（模式串指向第 5 个字符「a」）。按照 <code>next</code> 数组跳转：</p>
<ul>
<li><code>next[4] = 3</code>，跳到 $j = 3$：<code>p[3] =</code> 「a」，和失配字符（主串的 <code>c</code>）不一样？——不对，仔细看：</li>
</ul>
<p>注意：失配发生在 <code>p[j]</code> 与主串字符比较时。若 <code>p[j] == p[next[j]]</code>，那么跳到 <code>next[j]</code> 后，主串那个字符仍然不匹配，又会失配一次，然后又要跳到 <code>next[next[j]]...</code></p>
<ul>
<li>主串字符是 <code>c</code>，<code>p</code> 全是 <code>a</code>，所以无论 <code>j</code> 跳到哪，只要 <code>p[j] ==</code> 「a」，就必然再次失配。</li>
</ul>
<p>优化思路：与其跳到一个「已知字符相同、必然再次失配」的位置，不如一次跳到「字符真正不同」的位置（或者直接跳无可跳）。把这种压缩后的跳转表叫做 <code>nextval</code>。</p>
<h3>6.2 怎么计算：手算 nextval</h3>
<p>手算规则一句话：<code>nextval[j] = next[j]</code>，但若 <code>p[j] == p[next[j]]</code>，则 <code>nextval[j] = nextval[next[j]]</code>。也就是说：看 <code>next</code> 跳过去的那个字符和当前失配字符是否相同，相同就继续向前压缩（取 <code>nextval</code>），不同就直接用 <code>next</code>。</p>
<p>仍以 <code>"abaabacc"</code> 为例，<code>next = [-1, 0, 0, 1, 1, 2, 0, 1]</code>，逐位计算 <code>nextval</code>：</p>
<table>
<thead>
<tr>
<th>j</th>
<th>0</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
<th>5</th>
<th>6</th>
<th>7</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>p[j]</code></td>
<td>a</td>
<td>b</td>
<td>a</td>
<td>a</td>
<td>b</td>
<td>c</td>
<td>a</td>
<td>c</td>
</tr>
<tr>
<td><code>next[j]</code></td>
<td>-1</td>
<td>0</td>
<td>0</td>
<td>1</td>
<td>1</td>
<td>2</td>
<td>0</td>
<td>1</td>
</tr>
<tr>
<td><code>p[next[j]]</code></td>
<td>—</td>
<td>a</td>
<td>a</td>
<td>b</td>
<td>b</td>
<td>a</td>
<td>a</td>
<td>b</td>
</tr>
<tr>
<td>比较</td>
<td>—</td>
<td>b ≠ a 不同</td>
<td>a = a 相同</td>
<td>a ≠ b 不同</td>
<td>b = b 相同</td>
<td>c ≠ a 不同</td>
<td>a = a 相同</td>
<td>c ≠ b 不同</td>
</tr>
<tr>
<td><code>nextval[j]</code></td>
<td>-1</td>
<td>0</td>
<td>-1</td>
<td>1</td>
<td>0</td>
<td>2</td>
<td>-1</td>
<td>1</td>
</tr>
</tbody>
</table>
<p>对照结论：<code>nextval = [-1, 0, -1, 1, 0, 2, -1, 1]</code>。可以看到 $j = 2$、4、6 三处的值被压缩了：因为跳过去的字符和失配字符相同（都是 a/b/a），干脆再往前取。</p>
<h3>6.3 递推计算 nextval（O(M)）</h3>
<p>递推时并不需要先求完 <code>next</code> 再求 <code>nextval</code>，可以一遍递推同时得到 <code>next</code> 与 <code>nextval</code>：</p>
<pre><code>// 求模式串 ms 的 next 与 nextval（约定一：next[0] = -1）
vector&lt;int&gt; getNextValArray(const string&amp; ms) {
    int m = ms.size();
    if (m == 0) return {};
    vector&lt;int&gt; next(m);
    int i = 0, cn = -1; // cn 初始为 -1（对应 next[0]）
    next[0] = -1;
    // 原文此处代码不完整
}
</code></pre>
<p>这个写法直接产出 <code>nextval</code>（因为 <code>ms[i] != ms[cn]</code> 时存 <code>cn</code>，相同则存 <code>nxt[cn]</code>）。如果把关键行改成 <code>nxt[i] = cn</code>，得到的就是普通 <code>next</code> 数组。</p>
<p>复杂度同样是 $O(M)$：<code>i</code> 最多前进 <code>M</code> 次，<code>cn</code> 最多回退 <code>M</code> 次。</p>
<h3>6.4 匹配阶段怎么用 nextval</h3>
<p>匹配代码几乎不用改，只需把失配分支的跳转数组换成 <code>nextval</code>：</p>
<pre><code>// 匹配阶段：仅将 i2 = next[i2] 换成 i2 = nextval[i2]
} else {
    i2 = nextval[i2]; // 一跳到位，避免重复失配
}
</code></pre>
<p>好处：最坏情况下（如 <code>"aaaaab"</code> 这种大量相同字符的模式串），<code>nextval</code> 让跳转一步到位，匹配过程从「可能多次回退」变成「几乎不回退」，常数因子更小。复杂度量级不变（仍为 $O(N + M)$），但实际速度更快。</p>
<h3>6.5 next 与 nextval 对比</h3>
<table>
<thead>
<tr>
<th></th>
<th><code>next</code></th>
<th><code>nextval</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>含义</td>
<td>最长相等前后缀长度（跳转候选）</td>
<td>压缩多余跳转后的最终跳转位置</td>
</tr>
<tr>
<td>失配跳转</td>
<td><code>i2 = next[i2]</code></td>
<td><code>i2 = nextval[i2]</code></td>
</tr>
<tr>
<td>可能问题</td>
<td>跳过去字符若相同会再次失配</td>
<td>一跳到位，字符必不同或已跳无可跳</td>
</tr>
<tr>
<td>例 <code>"abaabcac"</code></td>
<td><code>[-1, 0, 0, 1, 1, 2, 0, 1]</code></td>
<td><code>[-1, 0, -1, 1, 0, 2, -1, 1]</code></td>
</tr>
<tr>
<td>适用场景</td>
<td>概念讲解、基础 KMP</td>
<td>追求常数速度优化</td>
</tr>
</tbody>
</table>
<p><code>nextval</code> 是 <code>next</code> 的「压缩版」：跳过去的字符如果和失配字符相同，就继续往前压缩，直到字符不同或跳无可跳为止。</p>
]]></content>
    <author><name>LxuMzn</name></author>
    <category term="数据结构与算法"/>
  </entry>
</feed>
