在我们日常的文本编辑、搜索引擎、生物信息学DNA序列比对等场景中,一个最基础又至关重要的问题是:如何在一个大的主串中找到一個我们关心的子串? 这个看似简单的过程,背后是计算机科学中经典的字符串匹配问题。
今天,我们将深入探讨解决这一问题的两位“主角”:简单直接的BF算法和高效智慧的KMP算法。理解它们,不仅能帮助我们解决实际问题,更能领略算法设计的巧妙之处。
一、BF算法 - 朴素的守护者
定义与特性
BF算法(Brute-Force算法),又称暴力匹配算法或朴素匹配算法。正如其名,它的核心思想非常直接:将主串的每一个字符作为子串的开头,与子串进行逐一匹配。如果匹配失败,则主串的指针回溯到本次起始位置的下一位,子串指针回溯到开头,重新开始下一轮匹配。
它是一种“傻瓜式”的算法,保证正确性但效率不高。
底层实现与思想
BF算法的思想可以用一个简单的流程来概括:
-
初始化两个指针
i
和j
,分别指向主串S
和子串P
的起始位置。 -
比较
S[i]
和P[j]
:-
如果相等,
i
和j
同时后移,继续比较下一个字符。 -
如果不相等,回溯:
i
指针后退到本次匹配起始位置的下一位(即i = i - j + 1
),j
指针重置回0(子串开头)。
-
-
重复步骤2,直到出现两种情况:
-
j
移动到了子串P
的末尾,说明匹配成功,返回当前主串匹配开始的起始位置i - j
。 -
i
移动到了主串S
的末尾,说明匹配失败,返回-1。
-
C++示例代码:
#include <iostream>
#include <string>
using namespace std;
int bruteForce(const string &mainStr, const string &pattern) {
int n = mainStr.length();
int m = pattern.length();
// i 是主串的指针
for (int i = 0; i <= n - m; ++i) {
int j;
// j 是子串的指针
for (j = 0; j < m; ++j) {
if (mainStr[i + j] != pattern[j]) {
break; // 字符不匹配,跳出内层循环
}
}
// 如果内层循环完整执行完毕,说明匹配成功
if (j == m) {
return i; // 返回匹配的起始位置
}
}
return -1; // 匹配失败
}
int main() {
string mainStr = "ABABCABCACBAB";
string pattern = "ABCAC";
int pos = bruteForce(mainStr, pattern);
if (pos != -1) {
cout << "Pattern found at index: " << pos << endl;
} else {
cout << "Pattern not found." << endl;
}
return 0;
}
// 输出:Pattern found at index: 5
应用场景
由于其实现简单,代码不易出错,BF算法常用于以下场景:
-
待匹配的子串长度较小时。
-
主串和子串的规模都不大时。
-
作为算法教学的入门案例,帮助理解字符串匹配的基本概念。
第二部分:KMP算法 - 智慧的跃迁
定义与特性
KMP算法(Knuth-Morris-Pratt算法),以其三位发明者的名字命名。它高效的核心在于利用已经匹配过的信息,在匹配失败时,通过一个预先计算好的“部分匹配表”(next
数组),让主串指针不回溯,只移动子串指针,从而极大地减少了不必要的比较次数。
底层实现与思想
KMP算法的精髓在于两点:next
数组和指针的巧妙移动。
1. 最大公共前后缀与next数组
next[j]
表示:在子串 P
中,从第0位到第 j-1
位(即前缀)的这个子串,其最长相等前后缀的长度。
-
前缀:指除了最后一个字符以外,字符串的所有头部组合。
-
后缀:指除了第一个字符以外,字符串的所有尾部组合。
-
最长公共前后缀:最长的、相同的前缀和后缀。
例如,子串 P = "ABCAC"
:
j | 子串 P[0:j] | 前缀 | 后缀 | 最长公共前后缀长度 | next[j] |
---|---|---|---|---|---|
0 | “” | - | - | -1 (特殊约定) | -1 |
1 | “A” | [“”] | [“”] | 0 (无相同) | 0 |
2 | “AB” | [“”, “A”] | [“”, “B”] | 0 | 0 |
3 | “ABC” | [“”, “A”, “AB”] | [“”, “C”, “BC”] | 0 | 0 |
4 | “ABCA” | [“”, “A”, “AB”] | [“”, “A”, “CA”] | 1 (“A”) | 1 |
5 | “ABCAC” | [“”, “A”, “AB”] | [“”, “C”, “AC”] | 0 | 0 |
这个 next
数组就是KMP算法的“行动指南”。
2. 匹配过程
匹配过程与BF类似,但失败时的处理截然不同:
-
初始化指针
i=0
(主串),j=0
(子串)。 -
比较
S[i]
和P[j]
:-
如果相等,
i++
,j++
。 -
如果不相等:
-
如果
j == 0
,说明子串第一个字符就不匹配,那么i++
(主串后移一位)。 -
否则,
j
指针不回溯,而是回退到next[j]
的位置,即j = next[j]
。
-
-
-
重复步骤2,直到
j
走到子串末尾(成功)或i
走到主串末尾(失败)。
C++示例代码:
#include <iostream>
#include <vector>
#include <string>
using namespace std;
// 构建 next 数组
vector<int> getNext(const string &pattern) {
int m = pattern.length();
vector<int> next(m, 0);
next[0] = -1; // 初始化 next[0]
int i = 0, j = -1; // i 是后缀末尾,j 是前缀末尾,也代表 next[i] 的值
while (i < m - 1) {
if (j == -1 || pattern[i] == pattern[j]) {
// 如果字符匹配,最长公共前后缀长度+1
i++;
j++;
next[i] = j;
} else {
// 不匹配时,j 回退到 next[j] 的位置
j = next[j];
}
}
return next;
}
// KMP 搜索算法
int kmpSearch(const string &mainStr, const string &pattern) {
int n = mainStr.length();
int m = pattern.length();
if (m == 0) return 0;
vector<int> next = getNext(pattern);
int i = 0, j = 0;
while (i < n && j < m) {
if (j == -1 || mainStr[i] == pattern[j]) {
// 字符匹配,指针后移
i++;
j++;
} else {
// 失配,子串指针 j 根据 next 数组回退
j = next[j];
}
}
if (j == m) {
return i - j; // 匹配成功
}
return -1; // 匹配失败
}
int main() {
string mainStr = "ABABCABCACBAB";
string pattern = "ABCAC";
int pos = kmpSearch(mainStr, pattern);
if (pos != -1) {
cout << "Pattern found at index: " << pos << endl;
} else {
cout << "Pattern not found." << endl;
}
return 0;
}
// 输出:Pattern found at index: 5
应用场景
KMP算法适用于对性能要求较高的场景:
-
在大规模文本中进行反复搜索。
-
子串具有明显的自我重复特性(此时
next
数组能发挥巨大优势)。 -
生物信息学中基因序列的比对。
-
各类文本编辑器、IDE的搜索功能底层实现。
第三部分:终极对决 - BF vs. KMP
为了更直观地展示两者的区别,我们用一个表格来总结它们的核心差异:
一个生动的比喻
-
BF算法就像一个认真的检查员,从第一页开始,一页一页、一个字一个字地核对手册,即使中途发现错误,也要回到下一页的开头重新开始核对。
-
KMP算法则像一个聪明的侦探。他记下了手册(子串)自身的规律(
next
数组)。当核对到某个词发现错误时,他不会回到手册开头重来,而是根据之前的规律,直接翻到手册中一个特定的位置继续核对,而文本(主串)则完全不用往回看。
总结
BF和KMP算法是字符串匹配领域的基石。BF算法以其极致的简单向我们展示了问题最原始的解法,而KMP算法则通过巧妙的预处理,将效率提升到了一个新的高度。
理解BF算法是基础,它能帮你建立起字符串匹配的直觉。而攻克KMP算法,特别是理解 next
数组的构建过程,则是对你算法思维的一次极好锻炼。下次当你在代码中调用 string.find()
时,或许可以想一想,它底层用的是哪种策略呢?
希望这篇博客能帮助你彻底理解这两种重要的算法!