字符串匹配问题:其主要功能给定两个字符串T和f(字串),长度分别为n和m,判断f是否在T中出现,如果出现则返回出现的位置。给定两个字符串T和f(字串),长度分别为n和m,判断f是否在T中出现,如果出现则返回出现的位置。如:有一个字符串"BBC ABCDAB ABCDABCDABDE",我想知道,里面是否包含另一个字符串"ABCDABD"?
朴素字符串匹配算法:遍历T的每一个位置,然后从该位置开始和f进行匹配,但是这种方法的复杂度是O(nm)。这种算法没有利用匹配过的信息,对于字串来说每次都从头开始比较,而对于主串来说,经常需要重复比较之前已经检测过的字符,因而速度很慢。模式匹配是一个常见的应用问题,用的广了,就有人想法去优化了。Rabin-Karp算法、有限自动机等等,前仆后继,最终出现了KMP(Knuth-Morris-Pratt)算法。
kmp算法被称为“看毛片”算法,是一个效率非常高的字符串匹配算法。
kmp算法通过一个O(m)的预处理来构建一个字串f的前缀数组(即计算字符串f每一个位置的字符串的前缀和后缀公共部分的最大长度,不包括字符串本身,否则最大长度始终是字符串本身),接下来的匹配过程会不断地使用到该前缀数组(而且对于主串T只需遍历一次),使匹配的复杂度降为O(n+m)。
构建前缀数组:
规定:next[0]=next[1]=0;
next[i]就是前缀数组,下面通过1个例子来看如何构造前缀数组。
例子1:cacca有5个前缀,求出其对应的next数组。
前缀2为ca,显然首尾没有相同的字符,next[2] = 0
前缀3为cac,显然首尾有共同的字符c,故next[3] = 1
前缀4为cacc,首尾有共同的字符c,故next[4] = 1
前缀5为cacca,首尾有共同的字符ca,故next[5] = 2
在本例中对于字串"ABCDABD"的前缀数组如下:
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
f(i) | A | B | C | D | A | B | D |
next[i] | 0 | 0 | 0 | 0 | 1 | 2 | 0 |
匹配过程:
1.
首先,字符串"BBC ABCDAB ABCDABCDABDE"的第一个字符与搜索词"ABCDABD"的第一个字符,进行比较。因为B与A不匹配,所以搜索词后移一位。
2.
因为B与A不匹配,搜索词再往后移。
3.
就这样,直到字符串有一个字符,与搜索词的第一个字符相同为止。
4.
接着比较字符串和搜索词的下一个字符,还是相同。
5.
直到字符串有一个字符,与搜索词对应的字符不相同为止。
6.
朴素字符串匹配过程:将搜索词整个后移一位,再从头逐个比较。这样做虽然可行,但是效率很差,因为你要把"搜索位置"移到已经比较过的位置,重比一遍。
而一个基本事实是,当空格与D不匹配时,你其实知道前面六个字符是"ABCDAB"。
KMP算法的想法:设法利用这个已知信息,不要把"搜索位置"移回已经比较过的位置,继续把它向后移,这样就提高了效率。而这时候就要利用到上面所产 生的前缀数组了,查表可知前面六个字符是"ABCDAB"的next[6]=2;因而直接将字串匹配位置移动6-2=4个位置;
即如下图:
移动位置=已匹配的字符数 - 对应的部分匹配值
7.
因为空格与C不匹配,搜索词还要继续往后移。这时,已匹配的字符数为2("AB"),对应的"部分匹配值"为0。所以,移动位数 = 2 - 0,结果 为 2,于是将搜索词向后移2位。
8.
因为空格与A不匹配,继续后移一位。
9.
逐位比较,直到发现C与D不匹配。于是,移动位数 = 6 - 2,继续将搜索词向后移动4位。
10.
逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 = 7 - 0,再将搜索词向后 移动7位,这里就不再重复了。
实现如下:
#include "iostream"
using namespace std;
void compute_prefix(int *next, char *p)
{
int i, n, k;
n = strlen(p);
next[1] = next[0] = 0;
k = 0; /* 第i次迭代开始之前,k表示next[i-1]的值 */
for (i = 2; i <= n; i++) {
for (; k != 0 && p[k] != p[i - 1]; k = next[k]);
if (p[k] == p[i - 1]) k++;
next[i] = k;
}
}
void kmp_match(char *text, char *p, int *next)
{
int m, n, s, q;
m = strlen(p);
n = strlen(text);
q = s = 0; /* q表示上一次迭代匹配了多少个字符,
s表示这次迭代从text的哪个字符开始比较 */
while (s < n) {
for (q = next[q]; q < m && p[q] == text[s]; q++, s++);
if (q == 0) s++;
else if (q == m) {
printf("pattern occurs with shift %d\n", s - m);
}
}
}
int main()
{
int next[101], n;
char *p = "ababababca";
char *text = "ababababcadababababcadababababcadababababca";
compute_prefix(next, p);
kmp_match(text, p, next);
cin.get();
return 0;
}