试图从真正意义的直观去描述KMP。
所谓的字符串匹配问题,就是比如说:
字符串A:"asdfadsfacbacb2acbacbacbcwerweacbacbacbc"
字符串B:"acbacbc"
需要在字符串A中寻找都有哪里出现了字符串B。现在请先直观的看,A中是出现了的B的,而且出现了两次。
直观的方法是所谓的BF算法,或者叫朴素匹配算法,就是从字符串A的第一个字符开始,匹配字符串B,对于上面的例子,则:
asdfads
acbacbc
在从1开始第2个字符时匹配失败,然后从A的第二个字符继续匹配:
sdfadsf
acbacbc
在从1开始第1个字符就又匹配失败,然后继续................
广义上来看,这个算法的时间复杂度是O(strlen(A - B + 1) * strlen(B)) = O(strlen(A) * strlen(B)),即所谓O(N * M),N是被匹配的字符串A的长度,M是模式串的长度,模式串就是匹配字符串B。BF算法在A很大时就有点慢了。
下面描述kmp算法,先举一个较为鲜明的例子描述kmp算法的优点:
字符串A:"acbacbacbacbd"
字符串B:"acbacbd"
第一次匹配会在字符串B的最后一个字符d处发现匹配失败,按照BF算法,接下来从A的从1开始第2个字符b开始继续匹配。先后是cbacbac、bacbacb再和字符串B继续匹配发现失败,然后是再是acbacba和字符串B匹配....直到最后匹配成功。
这里有2个问题:
1、cbacbac、bacbacb的匹配实质上不必要。试想字符串A和B,每个字符a之间的子串不是"cb",而是一大堆相同的字符串,那么无谓的匹配会不会更多;
2、如果字符串A前面有N多不相干字符,如字符串A是"3214243243253253253252352345234523523452acbacbacbacbdc",那么前面的匹配量很大但毫无意义。
KMP算法首先解决问题1,对KMP的一些改进一定程度解决问题2.
KMP算法最大的一个功用就是,如果模式串内部有重复的子串,比如"acbacbd","a"、"ac"、"acb"显然都是在B中重复出现的子串,用某种方式,在匹配被匹配字符串时,根据已经匹配成功的子串,不再机械的右移一位,而是去跳过不必要的匹配。
上面的描述肯定相当生涩,直接以前面的字符串A、B为例:
第一次匹配在B的最后一个字符d处失配,同时也说明前面"acbacb"的匹配都是已成功的,另外已知字符串B的子串acb在B中是2次重复出现的子串的,那么对字符串A的继续匹配,不再从A的从1开始第2位继续,而是从1开始第4位继续;
第二次匹配依然在B的最后一个字符d处失配,但是继续匹配也从A的从1开始第7位继续;
第三次匹配成功;
先试图思考如下几个特点:
1、如果模式串中包含"自子串",在发生失配时,查看失配前已成功的部分,是否包含这样的"内部自子串",如果包含就把下一个匹配位置提到已经匹配成功了的"内部自子串"的地点,跳过中间的不必要匹配;
2、如果模式串不包含"自子串,如模式串是"abcdefg"去匹配"abcdefhabcdefhabcdfg",那么在失配时有可能跳过更多不必要的匹配。第一次在模式串的字符g失配,下一次的匹配将从匹配串的第一个h开始,而不是从匹配串的第一个b开始。
思考一会这些特点。
KMP算法就是,在失配时根据已经匹配了的部分,再根据这部分当中是否有重复出现过的"自子串",合理安排下一次匹配的地点。
如果失配时,
1、没有已经匹配的部分,即第1个字符就匹配失败了,那么很显然就从第2个字符继续匹配吧;(如"abcde"匹配"12334324324")
2、有已经匹配的部分,且内部没有重复的"自子串",那么就从失配处继续匹配吧;(如"abcdefg"匹配"abcdefhabcdefhabcdefg")
3、有意见匹配的部分,且内部有重复的"自子串",那么找到"自子串"第2次出现的地方,继续匹配吧;(如"acbacbd"匹配"acbacbacbacbd")
根据上面的结论,KMP程序,需要在正式匹配强,首先找到模式串里的"自子串"并用某种方式标明,供匹配过程中使用,这也就是所谓的"next数组"问题。
1、寻找"自子串"(next数组):
先口头描述,然后上代码,以模式串"acbacbd"为例:
口头:
1、KMP的对模式串的内部重复情况的判断,都是以字符串首字符为开头的子字符串为特征,比如a、ac、acb是重复的子串,但是cb却不会被认为是重复的子串的。
2、KMP用数字的方式记录这些财富的情况,如对于acbacbd,记录为0001230,意义是:第一个字符是a默认记为0,后面除非再出现a,否则都记为0,所以后面的c、b先后记为0,再后面出现第二个a,和首字符a是相同字符,记为1,然后后面的c、b先后延续了第一个字符a后面的c、b,先后自增记为2、3,再出现d,不同于第4个字符是a,恢复记为0。
3、刚才的描述非常的不好(实在不知道怎么描述更通俗),再通俗一些,要被匹配的模式串肯定要从首字符开始匹配(或者从尾字符匹配),所以模式串里边的"自子串"必须也是从首字符(或者尾字符)开头(结尾)的,当出现了这样的"自子串",接下来再依次试图匹配首字符后面的字符是否匹配,如:
模式串acbacbd的从头匹配:首字符a记默认0,后面的c、d均记0,,然后出现字符a和首字符相同,记为1,接下来的c也和首字符后面的b相同,记为2,b同理记为3,后面的d恢复记为0,或者理解为需要继续从头去寻找a,最终next数组就是[0,0,0,1,2,3,0]。
这样的next数组转匹配时怎么用?比如被匹配的字符串是"acbacffffacbacbd",匹配到第一个f时失配,由于前面匹配成功,查看最后一个匹配成功的字符c的next值为2,那么直接由当前索引(f处的索引)向左移2位即可,即挪到从1开始第4位a处继续匹配。
再多举个例子,如模式串"abcdefg",显然next数组为全0,那么在匹配"abcdefhabcdefhabcdefg"时,第一次失配在第一个h处,前面全部成功匹配,最后一个匹配字符f的next值为0,那么直接由当前索引(h处索引)左移0位即可。
代码:
void make_pretbl (int *pretbl, const std::string pat) {
//首字符的next值默认0
pretbl[0] = 0;
int pre = 0;
for (int i = 1; i < strlen(pat.c_str()); i++) {
pre = pretbl[i - 1];
//pre用于标识"自子串"应该匹配的字符的索引
//在"自子串"已经存在时(pre > 0), 此时发生失配, 要让pre回到它应该重新匹配的地方
//如模式串是"abcabcabd", 在字符'd'处失配移到原始"自子串"的对应位置处(这里第一个abc就是"原始"自子串",
//d匹配c失败, 退回到b处这就是"对应位置处"), pre会由3变为0即重新试图找到>首字符a
while (pat[i] != pat[pre] && pre > 0) {
pre = pretbl[pre - 1];
}
//匹配, 继续自增next值; 失配, back to 0
if (pat[i] == pat[pre]) {
pretbl[i] = pre + 1;
} else {
pretbl[i] = 0;
}
}
}
代码中的pretbl就是所谓的next数组。之所以叫pretbl是因为这里是在做根据首字符的"自子串",还可以做根据尾字符的从后向前的"自子串",道理其实一样的,后面会描述。
下面看一下是怎么样做KMP匹配的(先不要关注代码中和posttbl有关的部分):
void kmp (const std::string raw, const std::string pat) {
int *pretbl = new int[strlen(pat.c_str())], *posttbl = new int[strlen(pat.c_str())];
make_pretbl(pretbl, pat);
make_posttbl(posttbl, pat);
size_t j = strlen(raw.c_str()) - 1;
size_t i = 0;
while (i < raw.size() && j >= 0) {
size_t idx = 0, idy = strlen(pat.c_str()) - 1;
if (i >= j) {
break;
}
//如果第一个字符就失配了, 就移到下一个字符吧...
//如果是有匹配成功的(idx>0), 这就应该看下应该移到哪里继续匹配,
//例1: raw: abcdefg, pat: 1234567, 第一个字符就失配了, 直接左移一位吧
//例2: raw: abcdefg, pat: abcdefh, 匹配到最后一个g字符失配, pretbl[idx - 1] = pretbl['f'] = 0, 前面匹配成功且无"自子串",故无需左移位
//例3: raw: abcabcabcc, pat: abcabcabca, 匹配到最后一个c字符失配, pretbl[idx - 1] = pretbl['c'] = 6, 前一个字符的pre为6就说明有"自子串", raw的索引i左移6位
while (i < raw.size()) {
if (raw[i] != pat[idx]) {
if (idx == 0) {
++i;
break;
} else {
i -= pretbl[idx - 1];
break;
}
} else {
if (idx == strlen(pat.c_str()) - 1) {
std::cout << "left-matched, position: " << i - strlen(pat.c_str()) + 1 << std::endl;
++i;
break;
} else {
++idx;
++i;
}
}
}
//后缀匹配同理:
//例1: raw: 1232143123aaaab, pat: abcde, 第0个字符e就失配, j直接右移1位吧
//例2: raw: 3241324213aaaab, pat: caaab, 第4个字符c失配, posttbl[idy + 1] = posttbl['a'] = 0, j无需右移
//例3: raw: 1243242141cabab, pat: aabab, 第4个字符a失配, posttbl[idy + 1] = posttbl['a'] = 2, j右移2位
while (j >= 0) {
if (raw[j] != pat[idy]) {
if (idy == strlen(pat.c_str()) - 1) {
--j;
break;
} else {
j += posttbl[idy + 1];
break;
}
} else {
if (idy == 0) {
std::cout << "right-matched, position: " << j << std::endl;
--j;
break;
} else {
--idy;
--j;
}
}
}
}
delete []pretbl;
delete []posttbl;
}
下面描述下posttbl是干什么用的,也就是说前面所说的从尾部匹配是什么意思,比如说对于被匹配字符串"12424353245335345435453453224aaaab",模式串为"aaaab",那么即便用刚才的KMP方法,也需要从左到右匹配很多次,才能匹配到。
再如一个更鲜明重要的例子,被匹配字符串是"aaaaaaaaaaaaaaaaaaaaab",模式串是"aaaab",如果仅按从左到右的方式,需要走过很多个a开头的匹配吧,但如果从右向左匹配呢,一下子减少了一大堆匹配。
所以说如果从头部和尾部同时进行匹配,对于上面这样的被匹配字符串就很快能匹配到了,其实思路和方式是完全一样的。即,创建另一个后缀next数组posttbl,寻找模式串中,以尾字符为开头、方向从右到左的的"内子串";在正式匹配中,对被匹配字符串,从右到左方向的去做匹配,方式和从左到右的匹配是完全一样的,代码仅根据方向变化做相应变化即可。
#include <string>
#include <iostream>
//前缀数组, 这个是干什么的?
//记录一个匹配串里的"自子串"
//什么是"自子串"? 如对于字符串"abcabcabca", 那么从0开始第3个字符a就是一个"自子串", 第3-4的子串ab也是一个"自子串",
//第3-5的子串abc也是一个"自子串", 第3-6的子串abcd也是一个"自子串", 第3-7的子串abcab也是一个"自子串", 第3-8的子串abcabc也是一个"自子串"
//获取"自子串"有什么用处? 比如被匹配的子串是"abcabcabcabcd", 那么这时就在体现kmp算法的一个用途了, 当失配时会直接移位到后面的"自子串"去再匹配, 省去了之间的无意义匹配
void make_pretbl (int *pretbl, const std::string pat) {
//首字符的next值默认0
pretbl[0] = 0;
int pre = 0;
for (int i = 1; i < strlen(pat.c_str()); i++) {
pre = pretbl[i - 1];
//pre用于标识"自子串"应该匹配的字符的索引
//在"自子串"已经存在时(pre > 0), 此时发生失配, 要让pre回到它应该重新匹配的地方
//如模式串是"abcabcabd", 在字符'd'处失配移到原始"自子串"的对应位置处(这里第一个abc就是"原始"自子串", d匹配c失败, 退回到b处这就是"对应位置处"), pre会由3变为0即重新试图找到首字符a
while (pat[i] != pat[pre] && pre > 0) {
pre = pretbl[pre - 1];
}
//匹配, 继续自增next值; 失配, back to 0
if (pat[i] == pat[pre]) {
pretbl[i] = pre + 1;
} else {
pretbl[i] = 0;
}
}
}
//后缀数组和前缀数组一样道理
void make_posttbl (int *posttbl, const std::string pat) {
posttbl[0] = 0;
int post = 0;
for (int i = strlen(pat.c_str()) - 2; i >= 0; i--) {
post = posttbl[i + 1];
while (pat[i] != pat[strlen(pat.c_str()) - 1 - post] && post > 0) {
post = posttbl[strlen(pat.c_str()) - post];
}
if (pat[i] == pat[post]) {
posttbl[i] = post + 1;
} else {
posttbl[i] = 0;
}
}
}
void kmp (const std::string raw, const std::string pat) {
int *pretbl = new int[strlen(pat.c_str())], *posttbl = new int[strlen(pat.c_str())];
make_pretbl(pretbl, pat);
make_posttbl(posttbl, pat);
size_t j = strlen(raw.c_str()) - 1;
size_t i = 0;
while (i < raw.size() && j >= 0) {
size_t idx = 0, idy = strlen(pat.c_str()) - 1;
if (i >= j) {
break;
}
//如果第一个字符就失配了, 就移到下一个字符吧...
//如果是有匹配成功的(idx>0), 这就应该看下应该移到哪里继续匹配,
//例1: raw: abcdefg, pat: 1234567, 第一个字符就失配了, 直接右移一位吧
//例2: raw: abcdefg, pat: abcdefh, 匹配到最后一个g字符失配, pretbl[idx - 1] = pretbl['f'] = 0, 前面匹配成功且无"自子串",故无需左移位
//例3: raw: abcabcabcc, pat: abcabcabca, 匹配到最后一个c字符失配, pretbl[idx - 1] = pretbl['c'] = 6, 前一个字符的pre为6就说明有"自子串", raw的索引i右移6位
while (i < raw.size()) {
if (raw[i] != pat[idx]) {
if (idx == 0) {
++i;
break;
} else {
i -= pretbl[idx - 1];
break;
}
} else {
if (idx == strlen(pat.c_str()) - 1) {
std::cout << "left-matched, position: " << i - strlen(pat.c_str()) + 1 << std::endl;
++i;
break;
} else {
++idx;
++i;
}
}
}
//后缀匹配同理:
//例1: raw: 1232143123aaaab, pat: abcde, 第0个字符e就失配, j直接右移1位吧
//例2: raw: 3241324213aaaab, pat: caaab, 第4个字符c失配, posttbl[idy + 1] = posttbl['a'] = 0, j无需右移
//例3: raw: 1243242141cabab, pat: aabab, 第4个字符a失配, posttbl[idy + 1] = posttbl['a'] = 2, j右移2位
while (j >= 0) {
if (raw[j] != pat[idy]) {
if (idy == strlen(pat.c_str()) - 1) {
--j;
break;
} else {
j += posttbl[idy + 1];
break;
}
} else {
if (idy == 0) {
std::cout << "right-matched, position: " << j << std::endl;
--j;
break;
} else {
--idy;
--j;
}
}
}
}
delete []pretbl;
delete []posttbl;
}
int main () {
std::string raw = "asdfadsfacbacb2acbacbacbcwerweacbacbacbcasdfabcabccssdf";
std::string pat = "abcabcc";
//raw = "acbacbacbacbd";
//pat = "acbacbd";
std::cout << "raw: " << raw << std::endl;
std::cout << "pat: " << pat << std::endl;
kmp(raw, pat);
return 0;
}