字符串处理 —— 单模式匹配 —— KMP 算法

【概述】

KMP 算法又称 Knuth-Morris-Pratt 字符串匹配算法,是由于 D.E.Knuth、J.H.Morris 和 V.R.Pratt 三人共同研究的,用于解决字符串匹配问题。

其核心思想是利用已经部分匹配的有效信息,来保持文本串指针不回溯,通过修改模式串指针,让模式串尽量地移动到有效的位置,因此,整个 KMP 的重点就在于当模式串中某个字符与文本串不匹配时,如何移动模式串指针。

KMP 算法的主要特点是:

  1. 需要对模式字符串做预处理;
  2. 预处理阶段需要额外的 O(m) 空间和复杂度;
  3. 匹配阶段与字符集的大小无关;
  4. 匹配阶段至多执行 2n-1 次字符比较;
  5. 对模式中字符的比较顺序时从左到右;

【算法原理】

如下图,以文本 T=ABACBCDHIJK,模式 P=ABAD 为例,前面三个字符是匹配的而 C 与 D 不匹配,可以利用已匹配的信息将模式指针 j 移到第 1 位上(指针从 0 开始),文本指针 i 位置不变

字符串处理 —— 单模式匹配 —— KMP 算法_第1张图片字符串处理 —— 单模式匹配 —— KMP 算法_第2张图片

同理,对于下图中的文本 T=ABCABCDHIJK 与模式 P=ABCABB,当 C 与 B 不匹配时,可利用已匹配信息,将模式指针 j 移动到第 2 位上,文本指针 i 位置不变

字符串处理 —— 单模式匹配 —— KMP 算法_第3张图片字符串处理 —— 单模式匹配 —— KMP 算法_第4张图片

通过上面两个例子,可以看出,当匹配失败时,文本指针 i 的位置不变,模式指针 j 要移动到位置 k,而位置 k 的性质满足:模式串最前面 k 个字符与 j 之前的最后 k 个字符相同,即:P[0\sim k-1]=P[i-k\sim j-1]

字符串处理 —— 单模式匹配 —— KMP 算法_第5张图片

当 T[i]!=P[j] 时,有:T[i-j \sim i-1]=P[0 \sim j-1]

根据P[0\sim k-1]=P[i-k\sim j-1]

可得:T[i-k\sim i-1]=P[0\sim k-1],因此可以判断直接将 j 移动到 k 无须再比较前面的 k 个字符

至此,关键的部分是求得 k,由于在模式 P 的每一个位置都可能发生不匹配,也就是说需要计算每一个位置 j 对应的 k,故而可以用一个数组 next[] 来保存,即令 next[j]=k 表示当 T[i]!=P[j] 时,j 指针的下一个位置

以下图为例,可以发现,当 j 为 0 时就不匹配,这个时候 j 已经在最左边了,不可能进行移动,需要调整的是将指针 i 后移一位,因此要对 next[] 数组初始化,即 next[0]=-1

字符串处理 —— 单模式匹配 —— KMP 算法_第6张图片

再以下图为例,当 j=1 时不匹配,因为他前面只有一个位置,那么显然指针 j 需要移到 0 位置

字符串处理 —— 单模式匹配 —— KMP 算法_第7张图片

对比以下两个图,可以发现,当 P[k]=P[j] 时,有 next[j+1]=next[j]+1

证明:

由于 P[j] 之前已经有 P[0\sim k-1]=P[i-k\sim j-1],即 next[j]=k

这个时候有 P[k]=P[j],那么可以得到 P[0 \sim k-1] + P[k] = P[j-k\sim j-1] + P[j]。

即:P[0 \sim k] = P[j-k \sim j],即:next[j+1] = k + 1,从而有:next[j+1]=next[j]+1

字符串处理 —— 单模式匹配 —— KMP 算法_第8张图片字符串处理 —— 单模式匹配 —— KMP 算法_第9张图片

而当 P[k] \:\:!=P[j] 时,对比以下两张图,可以发现只要令 k=next[k] 即可

字符串处理 —— 单模式匹配 —— KMP 算法_第10张图片字符串处理 —— 单模式匹配 —— KMP 算法_第11张图片

此时,next 数组已经求出,但还存在一个缺陷

以下图为例,显然得到的 next 数组是 {-1,0,0,1}

字符串处理 —— 单模式匹配 —— KMP 算法_第12张图片

如下图,当 C 与 B 不匹配时,应将模版指针 j 移动到第一个元素

字符串处理 —— 单模式匹配 —— KMP 算法_第13张图片

不难发现,由于后面的 B 已经不匹配了,那么前面的 B 也一定是不匹配的,因此这一步完全没有意义,同理,A 也一样

显然,发生问题的原因在于 P[j]=P[next[j]] ,因此加一个判断条件即可,值得注意的是,何时需要加上此条判断要根据实际情况

【应用】

KMP 算法的核心是求 next 数组,最大的应用除解决字符串匹配问题外,常用 next 数组来求循环节。

在未加判断的 next 数组中,其代表当前字符之前的字符串中,最大前缀与后缀匹配数,即 next[j]=k 代表 j 之前的字符串中有最大长度为 k 的相同前缀后缀。

例如:

i 0 1 2 3 4 5
p[i] a

b

c b a  
next[i] -1 0 0 0 0 1

对于长度为 n 的模式串,由于 next[j]=k 表示 p[1...i-1] 最大前缀与后缀匹配数,那么模式串第 1 位到 next[n] 位与模式串第 n-next[n] 位到第 n 位是匹配的

因此当 next[i]>0 时,i-next[i] 为字符串匹配的时候移动的位数,故而当 n%(n-next[n])=0 时,说明模式串中存在重复连续的子串,其长度为 len=n-next[n],那么字符串的最小周期 res=n/len=n/(n-next[n])

【算法实现】

1.求 next 数组

1)不加判断版

int next[N];
void getNext(char p[]){
    next[0]=-1;//初始化

    int len=strlen(p);//模式串长度
    int j=0;//模式指针j
    int k=-1;//位置k

    while(j

2)加判断版

int next[N];
void getNext(char p[]){
    next[0]=-1;//初始化

    int len=strlen(p);//模式串长度
    int j=0;//模式指针j
    int k=-1;//位置k

    while(j

2.匹配

int KMP(char t[],char p[]) {
    int tLen=strlen(t);//文本串长度
    int pLen=strlen(p);//模式串长度

    int i=0;//文本串指针
    int j=0;//模式串指针

    getNext();//获取next数组

    while(i

3.求模式串出现次数

int KMP(char t[],char p[]) {
    int tLen=strlen(t);
    int pLen=strlen(p);
    getNext(p);

    int res=0;
    
    int j=0;//初始化在模式串的第一个位置
    for(int i=0;i

4.求最小循环节长度

int next[N];
void getNext(char p[]){
    next[0]=-1;//初始化

    int len=strlen(p);//模式串长度
    int j=0;//模式指针j
    int k=-1;//位置k

    while(j

你可能感兴趣的:(字符串处理 —— 单模式匹配 —— KMP 算法)