字符串匹配——KMP算法(flag)史上最容易懂的KMP解析

flag史上最容易懂的KMP解析

如果要学习KMP的正确性请期待以后的博客


KMP概述

KMP算法是由 Knuth/Morris/Pratt K n u t h / M o r r i s / P r a t t 三个人设计的线性字符串匹配算法。
这个算法用到了一个函数“前缀函数”,这里称作 π(i) π ( i )

下面我们看几个概念

文本T:是要查询的目标文章,长度记作 T.len T . l e n

模式P:是你的字典词,长度记作 P.len P . l e n
也就是我们要在文本里面找模式出现的次数和位置。

P[1..10]代表模式P第一个位置到第十个位置的字符串。

偏移s,如果我们正在比较 T[s+1..s+P.len] T [ s + 1.. s + P . l e n ] P P ,那么第一个字符在T中的位置的前一个位置就是偏移s。可见,如果 s=0 s = 0 ,那么我们正在比较 T[1..P.len] T [ 1.. P . l e n ] P P

前缀:对于一个字符串,从第一个字符开始的,所有的子串(本身除外)。
例如,对于“abcde”,它的前缀有”a”,”ab”,”abc”,”abcd”。
请注意,”abcde”不算是它的前缀。

后缀:参照前缀的概念,后缀是:对于一个字符串,以最后一个字符结尾的,所有的子串(本身除外)。
例如,对于”abcde”,它的后缀有”e”,”de”,”cde”,”bcde”。
请注意,”abcde”不算是它的后缀。

前缀函数:见下:

前缀函数

这里前缀函数我们记作π(i)。这个函数是针对模式 P P 的。对于串P[1..i](P的一个从P[1]开始的子串),设它前缀集合为 A A ,设它的后缀集合是B,那么 π(i) π ( i ) 就是 AB A ∩ B 中长度最长的字符串的长度。
例如:有一个字符串为 "ababaca" " a b a b a c a "
i=5 i = 5 的时候, P[1..5]= P [ 1..5 ] = "ababa" " a b a b a "
它的前缀集合:

A={"a","ab","aba","abab"} A = { " a " , " a b " , " a b a " , " a b a b " }

它的后缀集合:
B={"a","ba","aba","baba"} B = { " a " , " b a " , " a b a " , " b a b a " }

所以,
AB={"ab","aba"} A ∩ B = { " a b " , " a b a " ′ }

我们可以发现在 AB A ∩ B 中,长度最长的字符串是 aba a b a ,长度为 3 3 ,所以π(5)=3
我们可以预处理这个模式的前缀函数:
这里写图片描述

前缀函数的功能

我们回顾朴素字符串匹配法,我们要枚举所有的偏移 s s ,然后在每个偏移都花上O(P.len)的时间去枚举匹配。总时间复杂度是 O(P.lenT.len) O ( P . l e n ∗ T . l e n )

前缀函数的用法

如果我们在考虑一个偏移 s s 的时候,前k个字符匹配,但是第 k+1 k + 1 个字符不匹配,我们直接跳到偏移 s=s+π(k) s ′ = s + π ( k ) 继续匹配。

这样跳偏移的作用

这样跳偏移,可以省略中间很多没有可能匹配到的偏移。很显然,我们跳到的偏移肯定是最有可能匹配的偏移。

这样跳的正确性

等待后续博客……

前缀函数的预处理

见下。我们先讲怎么匹配字符串。

字符串匹配

如果我们在考虑一个偏移 s s 的时候,前k个字符匹配,但是第 k+1 k + 1 个字符不匹配,我们直接跳到偏移 s=s+π(k) s ′ = s + π ( k ) 继续匹配。
下面给出图解:
如果我们在处理偏移 s=4 s = 4 的时候,匹配了5个字符,第6个字符不匹配:
这里写图片描述
我们取偏移 s=s+π(5)=4+3=7 s ′ = s + π ( 5 ) = 4 + 3 = 7
这里写图片描述
已经匹配的字符个数为 π(5)=3 π ( 5 ) = 3 个字符。

code

const int maxn=1000020;
char T[maxn],P[maxn];//两个字符串
int T_len,P_len;//两个字符串的长度
int pai[maxn];//pi函数
int ans=0;//P在T中出现的个数
void KMP()
{
    int q=0;//q记录了有几个字符一起匹配
    for(int i=1;i<=T_len;i++)//枚举呗
    {
        while(q>0&&P[q+1]!=T[i])//如果不匹配,那就默认pai[q]个字符已经
            q=pai[q];
        if(P[q+1]==T[i])//如果匹配,那就已经匹配的字符个数+1
            q++;
        if(q==P_len)
            ans++,q=pai[q];
    //如果已经匹配的个数跟模式的长度相同,那么就已经匹配成功
    }
}

预处理 π π 前缀函数

其实预处理 π π 前缀函数的过程,就是模式串自我匹配的过程。

code

自己理解吧!

int pai[maxn];
void get_p()
{
    pai[0]=0;
    int k=0;
    for(int q=2;q<=P_len;q++)
    {
        while(k>0&&P[k+1]!=P[q])
            k=pai[k];
        if(P[k+1]==P[q])
            k++;
        pai[q]=k;
    }
}

时间复杂度分析

我们先看字符串匹配的过程。
在最坏的情况下,

while(q>0&&P[q+1]!=T[i])
//如果不匹配,那就默认pai[q]个字符已经匹配
            q=pai[q];

这些while语句中,我们可以证明,总共执行次数 T.len ≤ T . l e n .所以,字符串匹配过程的时间复杂度为 O(T.len) O ( T . l e n )
同上,预处理时间复杂度为 O(P.len) O ( P . l e n ) .

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
KMP算法是一种字符串匹配算法,用于在一个文本串S内查找一个模式串P的出现位置。它的时间复杂度为O(n+m),其中n为文本串的长度,m为模式串的长度。 KMP算法的核心思想是利用已知信息来避免不必要的字符比较。具体来说,它维护一个next数组,其中next[i]表示当第i个字符匹配失败时,下一次匹配应该从模式串的第next[i]个字符开始。 我们可以通过一个简单的例子来理解KMP算法的思想。假设文本串为S="ababababca",模式串为P="abababca",我们想要在S中查找P的出现位置。 首先,我们可以将P的每个前缀和后缀进行比较,得到next数组: | i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | --- | - | - | - | - | - | - | - | - | | P | a | b | a | b | a | b | c | a | | next| 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 | 接下来,我们从S的第一个字符开始匹配P。当S的第七个字符和P的第七个字符匹配失败时,我们可以利用next[6]=4,将P向右移动4个字符,使得P的第五个字符与S的第七个字符对齐。此时,我们可以发现P的前五个字符和S的前五个字符已经匹配成功了。因此,我们可以继续从S的第六个字符开始匹配P。 当S的第十个字符和P的第八个字符匹配失败时,我们可以利用next[7]=1,将P向右移动一个字符,使得P的第一个字符和S的第十个字符对齐。此时,我们可以发现P的前一个字符和S的第十个字符已经匹配成功了。因此,我们可以继续从S的第十一个字符开始匹配P。 最终,我们可以发现P出现在S的第二个位置。 下面是KMP算法的C++代码实现:
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值