KMP
算法
算法介绍
Knuth-Morris-Pratt
字符串查找算法,简称为 “KMP算法”,常用于在一个文本串S内查找一个模式串P 的出现位置,这个算法由 Donald Knuth
、Vaughan Pratt
、James H.Morris
三人于 1977
年联合发表,故取这 3
人的姓氏命名此算法。
下面先直接给出 KMP
的算法流程:
假设现在文本串 \(S\) 匹配到 \(i\) 位置,模式串 \(P\) 匹配到 \(j\) 位置
- 如果 \(j = -1\),或者当前字符匹配成功(即 \(S[i] == P[j]\)),令 \(i++,j++\),继续匹配下一个字符;
- 如果 \(j != -1\),且当前字符匹配失败(即 \(S[i] != P[j]\)),则令 \(i\) 不变,\(j = next[j]\)。这意味着失配时,模式串 \(P\) 相对于文本串 \(S\) 向右移动了 \(j - next [j]\) 位。
换言之,当匹配失败时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的 \(next\) 值(\(next\) 数组的求解会在下文详细阐述),即移动的实际位数为:\(j - next[j]\),且此值大于等于 \(1\)。
很快,你也会意识到 \(next\) 数组各值的含义:代表当前字符之前的字符串中,有多大长度的相同前缀后缀。例如如果 \(next [j] = k\),代表 \(j\) 之前的字符串中有最大长度为 \(k\) 的相同前缀后缀。
此也意味着在某个字符失配时,该字符对应的 \(next\) 值会告诉你下一步匹配中,模式串应该跳到哪个位置(跳到 \(next [j]\) 的位置)。如果 \(next [j]\) 等于 \(0\) 或 \(-1\),则跳到模式串的开头字符,若 \(next [j] = k\) 且 \(k > 0\),代表下次匹配跳到 \(j\) 之前的某个字符,而不是跳到开头,且具体跳过了 \(k\) 个字符。
下面介绍如何求 \(next\)
\(next\) 数组的处理是 KMP
算法的精髓所在。
它的主要思路是:模式串自己与自己匹配。我们采用递推方法分析。假如已知 \(next[1]\) 到 \(next[i]\) ,如何求出 \(next[i+1]\)?
分类讨论。
假设模式串中下标为 \(next[i]\) 的字符和下标为 \(i+1\) 的字符相等,那么显然相等的前后缀长度最长可以是 \(next[i]+1\)。
代码实现
using namespace std;
const int maxn = 1e6 + 5;
int n, m;
int nxt[maxn];
char a[maxn], b[maxn];
int main()
{
int j = 0;
scanf("%s", a);
scanf("%s", b);
n = strlen(a);
m = strlen(b);
nxt[0] = nxt[1] = 0;
for (int i = 1; i < m; i++)
{
while (j && b[i] != b[j])
{
j = nxt[j];
}
if (b[i] == b[j])
{
nxt[i + 1] = ++j;
}
else
{
nxt[i + 1] = 0;
}
}
j = 0;
for (int i = 0; i < n; i++)
{
while (j && a[i] != b[j])
{
j = nxt[j];
}
if (a[i] == b[j])
{
j++;
}
if (j == m)
{
printf("%d\n", (i - m + 1) + 1);
j = nxt[j];
}
}
for (int i = 1; i <= m; i++)
{
printf("%d ", nxt[i]);
}
puts("");
return 0;
}
Sunday
算法
算法介绍
Sunday
算法由 Daniel M.Sunday
在 1990
年提出,它的思想跟 BM
算法很相似:
只不过 Sunday
算法是从前往后匹配,在匹配失败时关注的是文本串中参加匹配的最末位字符的下一位字符。
- 如果该字符没有在模式串中出现则直接跳过,即移动位数 = 模式串长度 + 1;
- 否则,其移动位数 \(=\) 模式串长度 \(-\) 该字符最右出现的位置(以 \(0\) 开始) \(=\) 模式串中该字符最右出现的位置到尾部的距离 \(+ 1\)。
举个例子
主串:a b c d e f g h i
模式串:f g h
- 左对齐,依次比较字符。
- 发现模式串首位(
f
)与主串首位(a
)无法匹配,主字符串参与匹配最末尾字符的下一个字符(d
)不存在于模式串中(f g h
),就需要将模式串向后移动(模式串长度 3 + 1
)位。(即 e
和 f
对齐)
\[\Huge{\Downarrow}\]
- 发现模式串首位(
g
)与主串首位(f
)无法匹配,主字符串参与匹配最末尾字符的下一个字符(h
)存在于模式串中(f g h
),将模式串向后移动(模式串中最右端的该字符到模式串末尾的距离 (3 - 3) + 1
)。(即让两个 h
对齐)
- 发现模式串首位(
f
)与主串首位(f
)匹配,继续遍历发现所有字符匹配成功。
代码实现
using namespace std;
const int maxNum = 1005;
int shift[maxNum];
int Sunday(const string &T, const string &P)
{
int n = T.length();
int m = P.length();
for (int i = 0; i < maxNum; i++)
{
shift[i] = m + 1;
}
for (int i = 0; i < m; i++)
{
shift[P[i]] = m - i;
}
int s = 0;
int j;
while (s <= n - m)
{
j = 0;
while (T[s + j] == P[j])
{
j++;
if (j >= m)
{
return s;
}
}
s += shift[T[s + m]];
}
return -1;
}
signed main()
{
string T, P;
while (true)
{
getline(cin, T);
getline(cin, P);
int res = Sunday(T, P);
if (res == -1)
{
cout << "主串和模式串不匹配。" << endl;
}
else
{
cout << "模式串在主串的位置为:" << res << endl;
}
}
return 0;
}