字符串匹配的KMP算法

来源:互联网 发布:java kafka producer 编辑:程序博客网 时间:2024/05/16 07:23

试图从真正意义的直观去描述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值默认0pretbl[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即重新试图找到首字符awhile (pat[i] != pat[pre] && pre > 0) {pre = pretbl[pre - 1];}//匹配, 继续自增next值; 失配, back to 0if (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;}





0 0
原创粉丝点击