KMP算法

  • 避免从头匹配:最长相同前缀后缀
  • next[]:实现最长相同前缀后缀的思路
  • 递推分析:最长相同前缀后缀,从哪里来
  • 实现 KMP 算法



 


避免从头匹配:最长相同前缀后缀

KMP 第一个线性的字符串匹配算法。

算法的优化就是不做无功用,暴力匹配算法每次不匹配时,会重新开始新匹配。

  • KMP 的优化在于,知道之前已经匹配的文本,避免从头匹配

那怎么知道之前已经匹配的文本呢?

  • 同时满足:最长前缀 = 最长后缀

比如 A A C D A A,最长前缀 AA,最长后缀 AA,匹配过程如下:

  • 文本:A A C D A A B B C C D D
  • 子串:A A C D A A D

最后一个字母没匹配上,暴力匹配的下一步:

  • 文本:A A C D A A B B C C D D
  • 子串: A A C D A A D

最后一个字母没匹配上,KMP 算法的下一步:

  • 文本:A A C D A A C D A A D
  • 子串: A A C D A A D

 


next[]:实现最长相同前缀后缀的思路

那我们怎么实现最长相同前缀后缀的思路呢?

我们用 next[] 保存串的最长相同前缀后缀。

  • next 的作用是,不匹配时,模式串应该从哪里开始重新匹配,避免无用功。

如下图,文本为 t,next为 LPS(最长相同前缀后缀):

KMP算法_子串


第一个字母 A 开始递推,因为前面没有其他字母,所以 LPS[0] = 0

第二个字母 B 和前面的字母 A,没有重合,所以 LPS[1] = 0

第三个字母 A 和前面的字母 A 重合了,所以 LPS[2] = 1

······

第六个字母 B 的最长相同前缀后缀是 A B A B,所以 LPS[6] = 4

 


递推分析:最长相同前缀后缀,从哪里来

KMP算法_c++_02

  • a:最长相同前缀后缀个数,相同的下标是 t[i-1] == t[a-1]

递推的初始条件:LSP[0] = 0,第一个字母没有前缀,一定等于 0。

假设现在已知第 i-1 个字母最长相同前缀后缀等于 a,即 LPS[i - 2] = a。

请问第 i 个字母的最长相同前缀后缀,是怎么来的?有俩种情况。

  • t[i] == t[a],最长相同前缀后缀 + 1
  • t[i] != t[a],最长相同前缀后缀不变

情况一,第 i 个字母也相同,最长相同前缀后缀 + 1:

KMP算法_后缀_03

if ( t[i] == t[a] )
	LPS[i] = LPS[i-1] + 1        // 或者 LPS[i] = a + 1

情况二:第 i 个字母不同,最长相同前缀后缀不变:

KMP算法_后缀_04

if ( t[i] != t[a] )
	LPS[i] = ?                   // 不相等时,从哪里来?

KMP算法_后缀_05


比如,现在 B 和 C 不相等,LPS[B] = 2,最长相同前缀后缀是:A B。

可怎么让计算机知道呢?

  • 最长相同前缀后缀:A B A
  • 次最长相同前缀后缀:A B

如果不相等,就需要看次最长相同前缀后缀:

KMP算法_c++_06


最左边的绿色和橙色块,与中间的绿色和红色块,是否相同。有俩种情况:

  • 相同,LPS[i] = 次最长相同前缀后缀(绿色块 + 橙色块)
  • 不相同,继续看 — 次次最长相同前缀后缀(绿色块)

我们看图可知,绿色块和红色块不同,但左边绿色块前面没有其他字母了,所以 LPS[i] = 0。

但是,我们需要把这个不断更新 最长相同前缀后缀 - 次次次次最长相同前缀后缀 的过程写出来:

KMP算法_开发语言_07

  • 绿色部分的长度是 LPS[a-1]
  • 最左边绿色部分最后一个字符位置是:LPS[a-1] - 1
  • 最左边绿色部分的下一个字符位置是:LPS[a-1]
  • 所以,不相等的情况,LPS[i] 从 t[ LPS[a-1] ] 来
a = LPS[i - 1]                        // 初始值是,最长相同前缀后缀
while ( a > 0 && t[i] != t[a] )   	  // 不相同时,不断更新
	a = LPS[a - 1]                	  // 把 a 替换成次最长相同前缀后缀

if ( t[i] == t[a] )                   // 相同时,从前一个来
	LPS[i] = LPS[i-1] + 1             // 或者 LPS[i] = a + 1

LPS 实现:

string get_next(string s) {
	int n = s.size();                           // 文本长度
	vector<int> LSP(n, -1);                     // 记录每个串的最长相同前缀后缀
	for (int i = 1; i < n; ++i) {               // 遍历每个串
		int j = LSP[i - 1];                     // 初始化是,最长相同前缀后缀
		while (j != -1 && s[j + 1] != s[i])     // 不相同时,不断更新
			j = LSP[j];                         // 替换为次~次次最长相同前缀后缀
            
		if (s[j + 1] == s[i])                   // 相同时
			LSP[i] = j + 1;                     // LSP[i] 从 LSP[i-1] + 1 而来
        }
        return s.substr(0, LSP[n - 1] + 1);     // 避免从头匹配,找到相同子串位置
    }
};

 


实现 KMP 算法

int KMP(char *chang,char *duan) {
    int *next = get_next(duan);
    
    int c_strlen = strlen(chang);
    int d_strlen = strlen(duan);
    int c=0, d=0;
    while(c<c_strlen && d<d_strlen){
 
        if( d==-1 || chang[c]==duan[d] )   // 暴力匹配部分,如果相同,俩个指针一起向后一位
            c++, d++;
        else 
            d = Next[d];        // 失配,指针回退到对应 Next[]下标,避免从头匹配   
    }
    return d<d_strlen?-1:c-d;
}