字符串匹配|kmp笔记

很久之前学的了。

我很懒,不太喜欢画图。

做个笔记回忆一下:

kmp

朴素比对字符串

所谓字符串匹配,是这样一种问题:“字符串 T 是否为字符串 S 的子串?如果是,它出现在 S 的哪些位置?” 其中 S 称为主串;T 称为模式串。如在字符串s abcabcabcabd 中找到子串T abcabd :

先设两个指针i、j,i表示S的指针,j表示T的指针

i=j=0
↓(i)
abcabcabcabd
abcabd
↑(j)

匹配成功,移动指针(i++,j++)

 ↓
abcabcabcabd
abcabd
 ↑

匹配成功,移动指针(i++,j++)

.
.
.

     ↓
abcabcabcabd
abcabd
     ↑

c≠d,回溯(i=1,j=0)

 ↓
abcabcabcabd
abcabd
↑

b≠a,回溯(i=2,j=0)

.
.
.

      ↓
abcabcabcabd
abcabd
↑

匹配成功,移动指针(i++,j++)

       ↓
abcabcabcabd
abcabd
 ↑

匹配成功,移动指针(i++,j++)

        ↓
abcabcabcabd
abcabd
  ↑

.
.
.

           ↓
abcabcabcabd
abcabd
     ↑

匹配成功,找到模式串(print(i))

优化

上面的复杂度是 O(nm) ,为什么这么多,发现是回溯花费时间过多。我们合理的希望是i不回溯,即:

先设两个指针i、j,i表示S的指针,j表示T的指针

i=j=0
↓(i)
abcabcabcabd
abcabd
↑(j)

匹配成功,移动指针(i++,j++)

 ↓
abcabcabcabd
abcabd
 ↑

匹配成功,移动指针(i++,j++)

.
.
.

     ↓
abcabcabcabd
abcabd
     ↑

c≠d,i不回溯,因为ab已经匹配完了,所以我们跳到上一个ab的位置(j=2)

     ↓
abcabcabcabd
abcabd
  ↑

匹配成功,移动指针(i++,j++)

      ↓
abcabcabcabd
abcabd
   ↑

匹配成功,移动指针(i++,j++)

       ↓
abcabcabcabd
abcabd
    ↑

匹配成功,移动指针(i++,j++)

        ↓
abcabcabcabd
abcabd
     ↑

a≠d,i不回溯(j=2)

        ↓
abcabcabcabd
abcabd
  ↑

匹配成功,移动指针(i++,j++)

.
.
.

           ↓
abcabcabcabd
abcabd
     ↑

匹配成功,找到模式串(print(i))

全程i不会减少

nxt数组

我们假设知道一个叫做nxt的数组,代表下一个j,当匹配失败时就可以 j=nxt[j] 来防止i的回溯。那么我们可以快速算出他的子串,如下代码:

int KMP(){
	for(int i=0,j=0;i<n;i++){
		while(j>0 && str[i]!=pnt[j]){
			j=nxt[j-1];      // 为什么是 nxt[j-1],因为第j位和第i位已经不匹配了,j-1位和i-1位才是匹配的,所以用j=nxt[j-1]
		}
		if(str[i]==pnt[j]){
			j++;             // 匹配成功
		}
		if(j==m){                // 匹配成功
			return i-j+1;
		}
	}
	return -1;
}

nxt数组是什么

nxt代表重复真子集长度,和回文串差不多,但不是回文串。区别

回文串:abccba
重复真子集:abcabc

欸,那么我们可以看出当已经有不匹配:

      ↓
abcabcabcabcd
abcabcd
      ↑

因为前面的abc已经匹配完了,我们不需要回溯回去再匹配,只需要跳到上一个abc的位置就行了。

     ↓
abcabcabcabcd
abcabcd
  ↑

我们nxt储存的就是与它重复的这部分的位置。以 abcababdabc 为例:

a:0(因为是真子集,不包括自身)

ab:0

abc:0

_  _
abca:1

__ __
abcab:2

_    _
abcaba:1

__   __
abcabab:2


abcababd:0

_       _
abcababda:1

__      __
abcababdab:2


___     ___
abcababdabc:3

那么我们会发现,他们重复这部分的下标(以0开始)刚好就是重复真子集长度:

有S=abcabcabd
T=abcabd

当匹配到:
     ↓
abcabcabd
abcabd
     ↑

时,说明前面的ab已经配好了,我们移动到上一个也有ab的地方:
     ↓
abcabcabd
   abcabd
     ↑
即可成功匹配

计算nxt数组

我们可以用递推的思想,先设有nxt[0]=0(必然的),然后设有快指针i=1,慢指针j=0,刚好,我们会发现重复部分的长度也是j的值。

对于匹配成功,则j++

对于匹配失败,则从上一位nxt中找到重复部分回溯j。

看不懂就看一下计算过程吧

计算abcabdabcabc的nxt,ij定义同上,上面箭头表示i,下面箭头表示j
 ↓(i)
abcabdabcabc
↑(j)

不相同,故nxt[i(1)]=0

  ↓(i++,下不再阐述)
abcabdabcabc
↑

不相同,故nxt[i(2)]=0,j不变(因为j是0,不必回溯)

   ↓
abcabdabcabc
↑

相同,故j++,nxt[i(3)]=1

    ↓
abcabdabcabc
 ↑

相同,故j++,nxt[i(4)]=2

     ↓
abcabdabcabc
  ↑

不相同,故j回溯到nxt[j-1(1)]的重复长度(0)

     ↓
abcabdabcabc
↑

无法再回溯,nxt[i(5)]=0


      ↓
abcabdabcabc
↑

相同,故j++,nxt[i(6)]=1

       ↓
abcabdabcabc
 ↑

相同,故j++,nxt[i(7)]=2

        ↓
abcabdabcabc
  ↑

相同,故j++,nxt[i(8)]=3

         ↓
abcabdabcabc
   ↑

相同,故j++,nxt[i(9)]=4

          ↓
abcabdabcabc
    ↑

相同,故j++,nxt[i(10)]=5


           ↓
abcabdabcabc
     ↑

不相同,故j回溯到nxt[j-1(4)]的重复长度(2)

           ↓
abcabdabcabc
  ↑

发现相等,j++,nxt[i(11)]=j=3

遍历完成,退出

代码如下:

void makeNext(){
	nxt[0]=0;
	for(int i=1,j=0;i<m;i++){
		while(j>0 && pnt[i]!=pnt[j]){
			j=nxt[j-1];    // 因为nxt表示重复部分的下标,我们可以回溯回去
		}
		if(pnt[i]==pnt[j]){
			j++;
		}
		nxt[i]=j;
	}
}

代码:

#include<cstdio> 
#include<cstring> 
#include<string>
char str[1010],pnt[1010];
int n,m;
int nxt[1010]; 
void makeNext(){
	nxt[0]=0;
	for(int i=1,j=0;i<m;i++){
		while(j>0 && pnt[i]!=pnt[j]){
			j=nxt[j-1];
		}
		if(pnt[i]==pnt[j]){
			j++;
		}
		nxt[i]=j;
	}
}
int KMP(){
	for(int i=0,j=0;i<n;i++){
		while(j>0 && str[i]!=pnt[j]){
			j=nxt[j-1];
		}
		if(str[i]==pnt[j]){
			j++;
		}
		if(j==m){
			return i-j+1;
		}
	}
	return -1;
}
int main(){
	scanf("%s %s",str,pnt);
	n=strlen(str);
	m=strlen(pnt);
	makeNext();
	printf("%d",KMP());
} 
posted @ 2023-05-29 13:36  ZnPdCo  阅读(50)  评论(0编辑  收藏  举报