Skip to content

朴素模式匹配(BF 算法)

2026 大纲 六(八)字符串模式匹配 · 朴素匹配部分(KMP 见《KMP 算法》,前置概念见《串的基本概念》)。

不用想就能想到的那个办法

要在主串 S 里找模式串 T,最朴素的念头是:T 摆到 S 的第 1 位上比一比,不行就整体右移一位再比,一直挪到挪不动为止。

T 一共能摆多少个位置?T 的末字符不能越过 S 的末字符,所以起点从 1 到 nm+1,共 nm+1 个对齐位置。每摆一个位置,就从头逐字符比 Tm 个字符:全对上就成功,中途撞上一个不等就作废,换下一个位置。

这就是 BF(Brute-Force,朴素/暴力匹配)的全部思想,穷举而已。它值得单独讲一节,不是因为算法本身有多难,而是因为它失败的方式,正好指出了 KMP 该往哪儿改。所以这一篇的重点不在"怎么写",在"哪几个字符是白比的"。

先看一眼

下面的可视化把两个指针的移动画了出来。看的时候注意一件事:每次失配之后,i 是不是往回退了。

加载可视化中...

看完你应该确认:j 一失配就归 1,i 也跟着退回本趟起点的下一位——主串里有些字符被读了不止一次

唯一需要想一下的一行:i = i - j + 2

代码本身很短,麻烦只有回退那一行。

c
// BF 算法(串下标从 1 开始,ch[0] 闲置不用)
// 返回 T 在 S 中首次出现的起始位置;失败返回 0
int BF(char S[], char T[], int n, int m) {
    int i = 1, j = 1;                 // i 指向主串当前比较位置,j 指向模式串当前比较位置
    while (i <= n && j <= m) {        // 两个都要判:j>m 是成功,i>n 是失败
        if (S[i] == T[j]) {
            i++;                      // 本对字符相等,两个指针同步后移
            j++;
        } else {
            i = i - j + 2;            // 关键行:退回本趟起点的下一位
            j = 1;                    // 模式串从头再来,前面比对过的信息全部作废
        }
    }
    if (j > m)
        return i - m;                 // i 已停在匹配段之后一位,匹配段占 [i-m, i-1]
    else
        return 0;                     // i 越界而 j 未越界,剩余主串已不够长
}

这一行不必背,两步就能推出来。 设本趟对齐的起点是 i0,也就是本趟从 S[i0]T[1] 开始比。现在比到 S[i]T[j] 时失配了,这说明 T[1..j-1]S[i0..i-1] 已经逐个匹配上——本趟已经成功比对了 j1 对字符。于是

i=i0+(j1)i0=ij+1

本趟作废,下一趟从 i0+1 开始,即 inew=ij+2

写完之后花两秒自检:取 j=1(第一个字符就失配)代入,得 inew=i1+2=i+1——主串指针只前进一位,完全符合直觉。+2 还是 +1,一试就分得清。

另外两处细节也是同一种推法:

  • 成功时返回 i - m。跳出循环时最后那次 i++ 已经执行完,i 停在匹配段末字符的下一位;匹配段占 [im, i1]m 格,起点自然是 i - m。这里不需要 ±1 修正,正是因为串下标从 1 起、ch[0] 闲置。
  • 判成功只能看 j > m。循环有两个出口:j > m 是模式串被完整比完(成功),i > n 是主串扫到头了(失败)。出来之后用 j > m 区分是哪一个——不能靠 i 是否越界判。失败返回 0 而不是 1,是因为位置从 1 开始计,0 天然不是合法位置。

走一遍,把浪费指出来

以主串 S="ababcabcac"、模式串 T="abcac" 为例(n=10m=5):

text
位置:  1 2 3 4 5 6 7 8 9 10
S   :  a b a b c a b c a c
T   :  a b c a c
对齐起点 i0比较过程本趟比较次数结果
11S1=a=T1S2=b=T2S3=aT3=c3失配于 i=3,j=3i33+2=2
22S2=bT1=a1失配于 i=2,j=1i21+2=3
33S3=aS4=bS5=cS6=a 均相等,S7=bT5=c5失配于 i=7,j=5i75+2=4
44S4=bT1=a1i41+2=5
55S5=cT1=a1i51+2=6
66S6=aS7=bS8=cS9=aS10=c 全相等5匹配成功j=6>m,返回 im=115=6

共 6 趟、16 次字符比较,返回位置 6。三笔浪费全在这张表里:

第一笔,比对结果被整体丢弃。 第 3 趟比了 4 个字符才失配,这 4 个字符的信息随着 j = 1 一起作废,下一趟从零开始。

第二笔,有些趟本来就不必发生。 第 3 趟其实已经"看到"了 S4=bS5=c(它们等于 T2T3),而 T1=a 与这两个都不等——第 4、5 趟注定失败,却还是老老实实比了两次。

第三笔,主串指针来回走。 i 走到 7 又退回 4,第 4~7 号字符被反复读取。这一笔的代价不只是时间:i 会回退,就意味着必须能随机访问主串,没法边读入边匹配。

第二笔浪费最关键,因为它暴露了一个可以利用的事实:

失配时,主串中已比对过的那一段,其内容恰好等于模式串的一个前缀。 既然它等于模式串的前缀,那么"下一个可能成功的对齐位置在哪"就只由模式串自己决定,与主串是什么完全无关——可以在还没看到主串的时候就算好。

这一句就是 KMP 的全部动机:把 j = 1 换成查一张表,i 也就没有回退的必要了。

复杂度:两个式子回答的是不同的问题

BF 有两个常被写在一起的式子,混用会直接算错。分清它们的办法是先问:这一趟是在哪个位置失配的。

最好情形——每趟都在模式串的第一个字符上失配。 例如 S="aaaaaba"T="ba",每趟只比 1 次就换位置。设匹配成功发生在第 i 个对齐位置,则前 i1 趟各比 1 次、第 i 趟比 m 次,总计 i1+m 次。假定 nm+1 个起点等概率,平均比较次数

i=1nm+11nm+1(i1+m)=nm2+m=n+m2

O(n+m)

最坏情形——每趟都拖到模式串的最后一个字符才失配。 想构造这种输入,只要让模式串前 m1 位全是同一个字符、末位换一个,主串则用那个字符铺满:

S=aaaaan1 个b,T=aaab(m=4)

n=8 走一遍:S="aaaaaaab"T="aaab",5 趟每趟都是"前三个 a 匹配上、第四个字符失配",各比 4 次,共 20 次。一般地,趟数 nm+1、每趟 m 次:

最坏比较次数=m(nm+1)

而按刚才同样的期望算法(前 i1 趟各 m 次、第 im 次,总 im):

i=1nm+11nm+1(im)=m(nm+2)2

两个式子的分工m(nm+1) 回答"最多比较多少次"(全部失败,或最后一趟才成功);m(nm+2)2 回答"平均比较多少次"(成功位置在各对齐点等概率下的期望)。量级都是 O(nm),但数值差一倍,问法不同不能换着用。

顺带一提:把 m(nm+1) 看成 m 的函数,它在 mn/2 处取到最大值 n2/4;而当 mn 时退化成 mn。两种写法都对。

最后是最容易被误解的一点:最坏 O(nm) 与"实际接近 O(n+m)"并不矛盾。 触发最坏情形,需要"模式串的前 m1 位能被反复匹配上",这要求主串与模式串高度自相似(大量重复字符)。而在自然文本、随机字符串这类输入上,绝大多数对齐位置在第 1 个字符就失配,每趟只比常数次,总量接近 O(n)。教材因此写道:BF 算法"在一般情况下,其实际的执行时间近似于 O(n+m),因此至今仍被采用"。

所以 KMP 的优势是有条件的:它买到的是最坏情形的保证,以及更本质的那条——主串指针不回溯,可以边读入边匹配。至于空间,BF 是 O(1)、不需要任何预处理数组,这是它唯一优于 KMP 的指标。

考点速记

三条会被反复调用的结论:

  1. 回退公式不背也能推:本趟起点 i0=ij+1,下一趟起点 i0+1=ij+2;拿 j=1 代入即可自检。
  2. 失配时主串已比对的那一段,内容恰好是模式串的一个前缀——这是与主串无关的已知量,能离线预处理。这一句就是 KMP 的全部动机。
  3. BF 空间 O(1)、无预处理,一般文本上实际接近 O(n+m),它不是被淘汰的算法;KMP 换来的是最坏保证与主串指针不回溯。

这一节在 408 真题里不单独成题,但它以另一种身份反复出现——KMP 题的错误选项。 整个「串」章的真题(下方「真题练习」)全部考 KMP,而这三道题里,每一道的干扰项都是"把 BF 的行为套到 KMP 上"

  • 2015 年那道问失配后的 ij,选项 A 给的是 i=1, j=0——这正是 BF 的回退方式(i 退回本趟起点的下一位、j 归零)。
  • 2019 年那道问比较次数,选项 D 给的是 15(约等于 |T| 的量级)——这是完全按 BF 逐趟回退算出来的结果。

换句话说,BF 在真题里的作用是"用来跟 KMP 对照的错误答案"。把这两条行为差异记牢,等于同时排掉 KMP 题里的一半选项:

失配时 i失配时 j
BF退到 ij+2归 1(或 0)
KMP不动jnext[j]

易错i 是否越界来判匹配成功。 唯一依据是 j > m

易错m(nm+1)m(nm+2)2 换着用。 前者答"最多",后者答"平均"。

易错认为 BF 已被 KMP 淘汰。 BF 空间 O(1)、常数小,一般文本上并不慢;KMP 赢的是最坏情形和"不回溯"。

教材出处
  • 严蔚敏、李冬梅、吴伟民《数据结构(C 语言版)》(第 2 版),第 4 章 4.3.3 节「串的模式匹配算法」,印刷第 92 页:BF 算法的算法步骤与算法描述 Index_BF,含"若不等,指针后退重新开始匹配,从主串的下一个字符(i=ij+2)起再重新和模式的第一个字符(j=1)比较"。
  • 同书印刷第 93 页:BF 算法分析。最好情形取 S="aaaaaba"T="ba",平均比较次数 12(n+m),时间复杂度 O(n+m);最坏情形每趟失配发生在模式串最后一个字符,平均比较次数 12m(nm+2),时间复杂度 O(n×m);并指出"BF 算法思路直观简明,但当匹配失败时,主串的指针 i 总是回溯到 ij+2 位置,模式串的指针总是恢复到首字符位置 j=1,因此算法时间复杂度高"。
  • 同书印刷第 93 页图 4.4:T="abcac" 与主串的六趟匹配过程示意,本篇的逐趟表与之一致。
  • 同书印刷第 97 页:「虽然 BF 算法的时间复杂度是 O(n×m),但在一般情况下,其实际的执行时间近似于 O(n+m),因此至今仍被采用」,以及 KMP 主串指针不回溯、可边读入边匹配的说明。

相关知识

串的基本概念(下标从 1 开始、ch[0] 闲置的约定,本篇代码的前提)| KMP 算法(把本篇指出的两笔浪费一次修掉)| 从 KMP 到工业级全文搜索(超纲延伸)| 查找的基本概念(按关键字等值比较,不覆盖字符串模式匹配

真题练习