c++字符串匹配之kmp

算法转载于:Knuth,Morris and Pratt


写在前面:字符串匹配是要在主串里找模式串
字符串匹配啊,在kmp之前,你会先想到什么?
蛮力啊!!(一听就是超时の典型)
很好,复杂度呢? O ( n × m ) O(n\times m) O(n×m)。不用说滴。燃鹅如果是随机的数据。它也会接近线性。假设范围是ascii和扩展,那么每一次匹配的第一次比较只有 1 256 \frac{1}{256} 2561的概率通过。。。
燃鹅大部分情景不是随机的啊!!
哈哈!蛮力算法再见吧!!
GoAway!!!!<(  ̄^ ̄)(θ(θ☆( >_<⭐


(以下场景纯属虚构,完全没有黑三位大神的意思)
正当你踌躇之时,你面前走来了三位嘉宾:
Knuth,Morris,Pratt
它们笑嘻嘻滴,可是把他们给开心坏了
他给你了一些指点:
kmp
对于蛮力算法来说,他没有利用以往的结果。以前算的,如果不匹配的话,就扔掉了。。。
为何不把tamen利用起来?
假设我们在i(对于模式串)位置跌倒,不匹配了,那么我们要找一个位置k(还是对于模式串)来再做比较。
为了保证正确性,需要满足:(p是模式串(pattern)) p [ : k ] = p [ ( i − k ) : i ] p[:k]=p[(i-k):i] p[:k]=p[(ik):i](这个使用到了python的切片)
额,会不会有一丁点抽象呢?
在k前面这一段(前缀),剪下来,名之为c
在i前面这一段,也给剪下来,然后取一个后缀,使得这个后缀长度与c相同,名之为b
应满足b=c
图呢?
kmp
对于一个i,有他对应的k,我们把它记录在一个表上,叫他next吧
燃鹅,我们没有构造方法啊
然后他们跟你说:你可以试用那个表的构造方法啊
然后你就试用了这个构造方法,做出来一段代码:

vector<int> kmp(string a, string b) {
    int n = a.size();
    int m = b.size();
    int* next = new int[m + 1];//God array
#pragma region BuildNext
//bulabulabulabulabulabulabulabulabula……
#pragma endregion
    int i = 0, j = 0;
    vector<int> ans;
    while (true) {
        //int ni = i;
        if (j == m) {
            ans.push_back(i - j);//i-j is the matched position.
            //ni = i - j + 1;//next position.
            j = next[j];
        }
        if (i == n)break;
        //i = ni;
        if (j < 0/*Guard*/ || a[i] == b[j]) {
            ++i, ++j;
        }
        else {
            j = next[j];
        }
    }
    delete[]next;
    return ans;
}

关于那个哨兵guard,
是当k(上文的,不是代码的)=-1的时候,也就是直接拿-1来比对,其实是直接过掉这个位置。
如果还有不懂得话,我这里有不少哨兵,且听下面分解。。


光阴似箭,30天过去了,TRIAL EXPIRE!!啊哈哈233试用期到了
~%?…,# *'☆&℃$︿★?
≡(▔﹏▔)≡
Pia!(o ‵-′)ノ”(ノ﹏<。)
(╯‵□′)╯炸弹!•••*~●
天不和尔意也,尔能咋地?
好吧,只能自己造一个next表了
思考中🤔……
嗯?一个前缀是另一个前缀的后缀?后面添加一对相同的一样成立?emmmm……
递推啊!!
哎,说起来麻烦,代码先上:

#pragma region BuildNext
    {//scope
        /*
        数组b有两个哨兵,
        b[-1]和b[m].
        当模式串一个也匹配不上这个字符的时候,返回-1。换句话说,过掉这个字符。
        当匹配成功时,我们假设b[m]是个刺头,跟谁都匹配不上,那么自然就要调用next[m]来和后面的比对了。
        */
        int t = next[0] = -1;//next[0] = -1。
        //t所在的位置满足:b[(j-t):(j+1)]  == b[:(t+1)]。如果把构建next表看作自己和自己比对的话,那么就差不多相当于上面的k,而下面的j就相当于上面的i。看后面自然就知道了
        int j = 0;//要改变的位置
        while (j < m) {
            if (t < 0 /*如果t是-1那个哨兵*/|| b[j] == b[t]) 
                //如果b[j] == b[t](b[(j-t):j]必须== b[:t]),所以 b[(j-t):(j+1)] 必定 == b[:(t+1)]
            {
                ++j, ++t;
                next[j] = (j == m || b[j] != b[t]) ? t : next[t];
                //如果b[j] == b[t],而且要和比较'X',
                //而且b[j] != 'X'。我们很容易知道b[t] != 'X',
                //但kmp还会比对。没事杀时间干啥?时间那么无辜(o´・ェ・`o)
                //赶紧叫kmp别干这种傻事!我们干脆直接拿next[t]来比对算了。
                //如果j==m,它就到后面的哨兵了。b[m] != b[t](我们事先曾假设'b[m] != any'。)
                //所以我们就用t啦。
            }
            else t = next[t];//试着用next[t]比较。
        }
    }//scope结束
#pragma endregion

注释贼多
首先,我们要知道 n e x t [ 0 ] ≡ − 1 next[0]\equiv -1 next[0]1
也就是当第0个位置都匹配不上的时候,那么很不幸,只能把哨兵叫出来,也就是过掉这个位置了~
为什么是-1哨兵呢?看图就明白了,其实就是形象化罢了。
-1哨兵
你看,是不是过掉了H?
其次定义一个t变量,它必须要满足 b [ ( j − t ) : j ] = b [ : t ] b[(j-t):j] = b[:t] b[(jt):j]=b[:t]
什么玩意??图跑那里去了??
j和t都是啥

如果这个位置ok相等了,那么下一个位置就确定了。自然j++,t++,next[j]=t。然后在看看下一个位置。
ok的情况

注意判断相等要看看t会不会是-1哨兵。那如果不ok呢??
不ok的情况

我们怎么办呢?在从0开始比对?
不知道你有没有想起字符串匹配的蛮力算法~
没错!就是那么慢!蜗牛一样慢🐌
那么你想办法优化,你又想到了kmp的next。。。
对了!就是用t=next[t]
t=next[t]

反正t也是前面的,next[t]也是求过的(这个时候可以想像构造next表就相当于模式串自己里面找自己。。。)
当然,有的时候next一次也不行,得两次、三次、四次。。。都可能。直到搞到-1或者相等。
你也许会有一个问题,为什么next[4]是0(在这个栗子里面,其中1是t现在指向的地方,4是j现在指的),而不是3?也就是说可能有的情况,到4的位置时,主串的值正好是A,而模式串是L,匹配失败,这个时候直接跳过了两个A,会不会出错误?当然不会啦(●’◡’●),要考虑到,都能混到4这个位置了,说明这个主串在j现在指的位置前面的部分一定长————HAHA这样(毕竟前面一定都是相同的),而如果切换到3的A,可是HAH不是HAHA的后缀,不行。位置1的A,H也不是HAHA的后缀,也不行,所以自然不会出现这种错误。(大神精心设计的算法还流传的这么广怎么可能有错?)现在的精华就在于这片代码了:

next[j] = (j == m || b[j] != b[t]) ? t : next[t];
                //如果b[j] == b[t],而且要和'X'比较,
                //而且b[j] != 'X'。我们很容易知道b[t] != 'X',
                //但kmp还会比对。没事杀时间干啥?时间那么无辜(o´・ェ・`o)
                //赶紧叫kmp别干这种傻事!我们干脆直接拿next[t]来比对算了。
                //如果j==m,它就到后面的哨兵了。b[m] != b[t](我们事先曾假设'b[m] != any'。)
                //所以我们就用t啦。
     

优化1
举个栗子吧
惨栗
看到了吗?红色的地方都在浪费时间。。。
那么,这里要防着点这种情况,我们设x是某个和b[j]匹配失败的字符,大致就是:
b [ j ] ≠ X b[j]\not = X b[j]=X
b [ j ] = b [ t ] b[j] = b[t] b[j]=b[t]
∴ b [ t ] ≠ X \therefore b[t]\not= X b[t]=X
但是kmp仍然比较 b [ t ] ⋚ X b[t]\lesseqgtr X b[t]X,有这时间干点别的不行啊?!?!
所以呢,就直接用next[t]算了。


优化2
注意如果j==m,这就是另一个哨兵了。
当匹配完成时,再从i-j+1的位置开始又是那个蛮力的味道了,
所以我们使用一个next[m]来比对
那么这里要思考🤔了,因为他要使用一个next[m]来替代,什么时候需要替代?不相等啊!
所以设: ∀ X ∈ A S C I I , b [ m ] ≠ X \forall X\in ASCII,b[m]\neq X XASCII,b[m]=X
也就是b[m]什么都不是,什么都匹配不上
那么这里就当它不等于b[t]了,所以使用t。
那么附上完整代码:

#include <bits/stdc++.h>
using namespace std;
//Knuth Morris Pratt
vector<int> kmp(string a, string b) {
    int n = a.size();
    int m = b.size();
    int* next = new int[m + 1];//神仙数组
#pragma region BuildNext
    {//scope
        /*
        数组b有两个哨兵,
        b[-1]和b[m].
        当模式串一个也匹配不上这个字符的时候,返回-1。换句话说,过掉这个字符。
        当匹配成功时,我们假设b[m]是个刺头,跟谁都匹配不上,那么自然就要调用next[m]来和后面的比对了。
        */
        int t = next[0] = -1;//next[0] = -1。
        //t所在的位置满足:b[(j-t):(j+1)]  == b[:(t+1)]。如果把构建next表看作自己和自己比对的话,那么t就差不多相当于上面的k,而下面的j就相当于上面的i。看后面自然就知道了
        int j = 0;//要改变的位置
        while (j < m) {
            if (t < 0 /*如果t是-1那个哨兵*/|| b[j] == b[t]) 
                //如果b[j] == b[t](b[(j-t):j]必须== b[:t]),所以 b[(j-t):(j+1)] 必定 == b[:(t+1)]
            {
                ++j, ++t;
                next[j] = (j == m || b[j] != b[t]) ? t : next[t];
                //如果b[j] == b[t],而且要和比较'X',
                //而且b[j] != 'X'。我们很容易知道b[t] != 'X',
                //但kmp还会比对。没事杀时间干啥?时间那么无辜(o´・ェ・`o)
                //赶紧叫kmp别干这种傻事!我们干脆直接拿next[t]来比对算了。
                //如果j==m,它就到后面的哨兵了。b[m] != b[t](我们事先曾假设'b[m] != any'。)
                //所以我们就用t啦。
            }
            else t = next[t];//试着用next[t]比较。
        }
    }//scope结束
#pragma endregion
    int i = 0, j = 0;
    vector<int> ans;
    while (true) {
        //int ni = i;
        if (j == m) {
            ans.push_back(i - j);//i-j is the matched position.
            //ni = i - j + 1;//next position.
            j = next[j];
        }
        if (i == n)break;
        //i = ni;
        if (j < 0/*Guard*/ || a[i] == b[j]) {
            ++i, ++j;
        }
        else {
            j = next[j];
        }
    }
    delete[]next;
    return ans;
}
int main()
{
    //std::cout << "Hello World!\n";
    std::ios::sync_with_stdio(false);
    string a, b;
    cin >> a >> b;
    vector<int> res = kmp(a, b);
    for (auto i : res)cout << i << '\n';
    return 0;
}

然后三位大佬转过头来找你要专利费


复杂度?反正你知道很快就行了o(* ̄▽ ̄*)o
因为构造next表时都不回头,使用时也不回头,可以当成一个正儿八经的for循环,∴所以,复杂度线性O(n)


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值