浅谈KMP

文章介绍了KMP算法用于字符串匹配的过程,包括暴力求解方法的时间复杂度为O(mn),然后详细解释了KMP算法如何通过构造失配数组fail[]优化匹配过程,降低复杂度。在匹配失败时,通过失配数组找到下一个可能的匹配起点,从而避免不必要的回溯。文章还涵盖了如何寻找模式串在文本串中的所有出现位置。
摘要由CSDN通过智能技术生成

前言

最早前写过一篇关于如何寻找子串在主串中的出现位置,详见如何在主串中寻找子串的出现次数
具体就是调用strstr函数,可以做到O(n+m)的线性复杂度,那么内部如何实现呢?
答案是KMP

继续下去之前请思考这样一个问题,给两个非空字符串str1, str2 如何寻找str2在str1中出现的第一个位置?所有位置呢?

暴力求解

  • 引入两个概念,文本串 模式串。一般说的是求模式串在文本串中的出现位置,了解这个说法就行
  • 其次我们在下文中会使用指针这个说法,不要在意,它并不是c语言中那个令人头疼的指针,把他理解为一个箭头就行

    如上图中,txt代表文本串,S代表模式串,而P1,P2分别指向两字符串中的某一个字符

在暴力求解时,每当 TXT[ P1 ] == S[ P2 ] 时,P1,P2指针同时向后移动 , 当然最好的情况是该条件一直成立

  • 当P2移动到模式串的结尾时(并且上述式子成立),则代表了模式串在文本串中出现了一次,此时P2 - S.length 就是模式串在文本串中的第一次出现位置

如果P2没有移动到模式串结尾,但是上述式子不成立怎么办?我们称这个为:失配

此时就要移动P1 P2指针到正确位置重新匹配, p1 = p1 - p2 + 2 , p1 = 1

void findPos(string txt, string s){
    int txtLen = txt.length();
    int sLen = s.length();
    int p1 = 0, p2 = 0;
    
    while(p1 < txtLen){
        //开始匹配
        while(p2 < sLen && txt[p1] == s[p2]){
            p1++,p2++;
        }
        
        if(p2 == sLen){     //找到了一个位置
            //对应操作
        } else{     //失配了,重新调整两个指针
            p1 = p1 - p2 + 2;
            p2 = 0;
        }
    }
}
//暴力求解代码不保证正确性

那么他的复杂度怎么样呢?因为每一次失配后p1指针都要移动到匹配模式串时的下一个位置,也即是p1 = p1-p2+2,相当于p1在匹配前的基础上+1,那么复杂度为O(txtLen*sLen) = O(mn)

KMP

经过上面的暴力写法可以看出,想要优化时间,必须想办法解决失配之后p1,p2指针向前移动的距离
下面给出一个例子,我们来看看他的p1,p2指针该如何移动:

可以看到前面abcx…都匹配成功了,但是好可惜,它在p1,p2的下一个位置失配了!
3秒中思考p1, p2指针最佳移动是什么?
3
2
1

https://img-blog.csdnimg.cn/img_convert/ea4987a56286cd8a29baace1eab1f497.jpeg
最佳移动如上图!

发现p1并没有移动,而p2移动到了一个特别的地方,为什么特别呢?从图中可以看出p2往前是ab,而没移动之前p2的前面也是ab,我们称这个为最长前缀匹配 想到了嘛?

移动之后我们要看p1,p2的下一个位置是否匹配,如果还是不匹配,是不是也是失配了?那么移动和上述过程一样!移动到他的最长前缀匹配处!但是看图发现p2在的地方貌似没有匹配到的前缀了,那就直接把p2设置为0,这里我们假设字符串的第一个位置是1

现在假设我们移动p2到了位置0,现在p2已经不能往哪里移动了,p1开始往下移动一位,然后和p2的下一个位置进行比较
在这里插入图片描述
这时发现p2的下一个位置和p1指向的字符匹配成功了!,那么p2移动到下一个位置也即是p2++

然后再次判断p2下一个位置和p1指向字符是否匹配
在这里插入图片描述

发现没?我们回到了最初的情况. 接下来就是重复上诉过程了
如果在其中p2奇迹般移动到了S的结尾,则说明匹配到了S在txt中的一个位置

构造失配数组 fail[ ]

上诉过程中P1指针的移动并没有什么特色,就只是p1++而已
但是p2在失配之后,需要根据最长前缀来选择移动位置,称为失配之后移动到的位置,个个位置弄一起就是失配数组了。 那么如何构造它呢?

无可厚非失配数组只和模式串有关,下面我们开始只关注模式串
在这里插入图片描述
当前p指向的位置的最长匹配前缀为2 (ab)
如果我们要构建p的下一位(记为位置 i)字符‘c’的最长匹配前缀呢?判断S[ fail[ p ] + 1 ] 是否也是‘c’就行,

  • 如果是则 fail [ i ] == fail [ p ] + 1
  • 如果不是?那么我们能不能理解为不符合S[fail[p]+1] == S[i]而叫做失配呢?,那么这时操作和文本串和模式串匹配时的操作一样,p 当前位置失配,p移动到他的最长前缀匹配处, 也即是 p = fail[ p ] , 接着再次判断上述式子是否成立,或者p是否移动到了0
    那么构造失配指针代码如下:
char str[maxn], s[maxn];    //文本串,模式串
int fail[maxn];     //失配指针

void getFail(){     //求出失配指针
    int len = strlen(s+1);	//假设s初始位置为1
    int tmp = fail[1] = 0;		//tmp则是上面讨论的p指针, 第一个位置当然最长匹配前缀为0
    for(int i=2;i<=len;i++) {
        while(tmp != 0 && s[tmp+1] != s[i]) tmp = fail[tmp];		//失配,并且p指针不指向 0 
        if(s[i] == s[tmp+1]) tmp++;
        fail[i] = tmp;
    }


}

寻找位置

构造完成失配数组之后就可以开始和文本串匹配了,过程就不在赘述了,代码如下:

void findPos(){     //在文本串中寻找位置

    int lenStr = strlen(str+1);		//一样的,假设串的初始位置为 1
    int lenP = strlen(p+1);
    int tmp = 0;		//tmp 也就是指向模式串位置的指针 P2

    for(int i=1;i<=lenStr;i++) {		// i 就是指向文本串位置的指针 P1
        while(tmp !=0 && p[tmp+1] != str[i]) tmp = fail[tmp];		//失配 
        if(p[tmp+1] == str[i]) tmp++;
        if(tmp == lenP){		// 匹配成功!!!
            cout<<i-tmp+1<<endl;
            tmp = fail[tmp];
        }
    }
}

多位置匹配

看匹配成功之后的那小段代码, 第一句是输出出现位置毫无疑问
那么第二句呢? tmp = fail [tmp] 这貌似是失配才会发生的操作

请考虑如何找出模式串在文本串中出现的所有位置?
egs:
文本串: aabaabaaba
模式串: aabaa

假设字符串位置从1开始,那么所有位置应该是1 , 4
在这里插入图片描述
上图可以看出,p2已经到了S的结尾,说明找到了一个位置,那么p2 = fail [p2]会移动到S的第二个位置
在这里插入图片描述
最后可以思考一下: 为什么要tmp = fail [ tmp ] 如果tmp = 0又是一个什么情况

end

练习题地址:洛谷-KMP模板题

感谢看到最后 😃 😃

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值