Appearance
朴素模式匹配(BF 算法)
不用想就能想到的那个办法
要在主串
这就是 BF(Brute-Force,朴素/暴力匹配)的全部思想,穷举而已。它值得单独讲一节,不是因为算法本身有多难,而是因为它失败的方式,正好指出了 KMP 该往哪儿改。所以这一篇的重点不在"怎么写",在"哪几个字符是白比的"。
先看一眼
下面的可视化把两个指针的移动画了出来。看的时候注意一件事:每次失配之后,
看完你应该确认:
唯一需要想一下的一行: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 未越界,剩余主串已不够长
}这一行不必背,两步就能推出来。 设本趟对齐的起点是 S[i0] 与 T[1] 开始比。现在比到 S[i] 与 T[j] 时失配了,这说明 T[1..j-1] 与 S[i0..i-1] 已经逐个匹配上——本趟已经成功比对了
本趟作废,下一趟从
写完之后花两秒自检:取 +2 还是 +1,一试就分得清。
另外两处细节也是同一种推法:
- 成功时返回
i - m。跳出循环时最后那次i++已经执行完,i停在匹配段末字符的下一位;匹配段占共 格,起点自然是 i - m。这里不需要修正,正是因为串下标从 1 起、 ch[0]闲置。 - 判成功只能看
j > m。循环有两个出口:j > m是模式串被完整比完(成功),i > n是主串扫到头了(失败)。出来之后用j > m区分是哪一个——不能靠i是否越界判。失败返回 0 而不是,是因为位置从 1 开始计,0 天然不是合法位置。
走一遍,把浪费指出来
以主串
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| 趟 | 对齐起点 | 比较过程 | 本趟比较次数 | 结果 |
|---|---|---|---|---|
| 1 | 1 | 3 | 失配于 | |
| 2 | 2 | 1 | 失配于 | |
| 3 | 3 | 5 | 失配于 | |
| 4 | 4 | 1 | ||
| 5 | 5 | 1 | ||
| 6 | 6 | 5 | 匹配成功, |
共 6 趟、16 次字符比较,返回位置 6。三笔浪费全在这张表里:
第一笔,比对结果被整体丢弃。 第 3 趟比了 4 个字符才失配,这 4 个字符的信息随着 j = 1 一起作废,下一趟从零开始。
第二笔,有些趟本来就不必发生。 第 3 趟其实已经"看到"了
第三笔,主串指针来回走。
第二笔浪费最关键,因为它暴露了一个可以利用的事实:
失配时,主串中已比对过的那一段,其内容恰好等于模式串的一个前缀。 既然它等于模式串的前缀,那么"下一个可能成功的对齐位置在哪"就只由模式串自己决定,与主串是什么完全无关——可以在还没看到主串的时候就算好。
这一句就是 KMP 的全部动机:把 j = 1 换成查一张表,
复杂度:两个式子回答的是不同的问题
BF 有两个常被写在一起的式子,混用会直接算错。分清它们的办法是先问:这一趟是在哪个位置失配的。
最好情形——每趟都在模式串的第一个字符上失配。 例如
即
最坏情形——每趟都拖到模式串的最后一个字符才失配。 想构造这种输入,只要让模式串前
取
而按刚才同样的期望算法(前
两个式子的分工:
顺带一提:把
看成 的函数,它在 处取到最大值 ;而当 时退化成 。两种写法都对。
最后是最容易被误解的一点:最坏
所以 KMP 的优势是有条件的:它买到的是最坏情形的保证,以及更本质的那条——主串指针不回溯,可以边读入边匹配。至于空间,BF 是
考点速记
三条会被反复调用的结论:
- 回退公式不背也能推:本趟起点
,下一趟起点 ;拿 代入即可自检。 - 失配时主串已比对的那一段,内容恰好是模式串的一个前缀——这是与主串无关的已知量,能离线预处理。这一句就是 KMP 的全部动机。
- BF 空间
、无预处理,一般文本上实际接近 ,它不是被淘汰的算法;KMP 换来的是最坏保证与主串指针不回溯。
这一节在 408 真题里不单独成题,但它以另一种身份反复出现——KMP 题的错误选项。 整个「串」章的真题(下方「真题练习」)全部考 KMP,而这三道题里,每一道的干扰项都是"把 BF 的行为套到 KMP 上":
- 2015 年那道问失配后的
、 ,选项 A 给的是 i=1, j=0——这正是 BF 的回退方式(退回本趟起点的下一位、 归零)。 - 2019 年那道问比较次数,选项 D 给的是 15(约等于
的量级)——这是完全按 BF 逐趟回退算出来的结果。
换句话说,BF 在真题里的作用是"用来跟 KMP 对照的错误答案"。把这两条行为差异记牢,等于同时排掉 KMP 题里的一半选项:
| 失配时 | 失配时 | |
|---|---|---|
| BF | 退到 | 归 1(或 0) |
| KMP | 不动 |
易错:用
i是否越界来判匹配成功。 唯一依据是j > m。
易错:把
与 换着用。 前者答"最多",后者答"平均"。
易错:认为 BF 已被 KMP 淘汰。 BF 空间
、常数小,一般文本上并不慢;KMP 赢的是最坏情形和"不回溯"。
教材出处
- 严蔚敏、李冬梅、吴伟民《数据结构(C 语言版)》(第 2 版),第 4 章 4.3.3 节「串的模式匹配算法」,印刷第 92 页:BF 算法的算法步骤与算法描述
Index_BF,含"若不等,指针后退重新开始匹配,从主串的下一个字符()起再重新和模式的第一个字符( )比较"。 - 同书印刷第 93 页:BF 算法分析。最好情形取
、 ,平均比较次数 ,时间复杂度 ;最坏情形每趟失配发生在模式串最后一个字符,平均比较次数 ,时间复杂度 ;并指出"BF 算法思路直观简明,但当匹配失败时,主串的指针 总是回溯到 位置,模式串的指针总是恢复到首字符位置 ,因此算法时间复杂度高"。 - 同书印刷第 93 页图 4.4:
与主串的六趟匹配过程示意,本篇的逐趟表与之一致。 - 同书印刷第 97 页:「虽然 BF 算法的时间复杂度是
,但在一般情况下,其实际的执行时间近似于 ,因此至今仍被采用」,以及 KMP 主串指针不回溯、可边读入边匹配的说明。
相关知识
串的基本概念(下标从 1 开始、ch[0] 闲置的约定,本篇代码的前提)| KMP 算法(把本篇指出的两笔浪费一次修掉)| 从 KMP 到工业级全文搜索(超纲延伸)| 查找的基本概念(按关键字等值比较,不覆盖字符串模式匹配)