基本思想:
我们在做字符串匹配类型的题目时,通常会想到用暴力匹配来做,但是由于复杂度为O(n*m)而爆掉,就有了KMP将时间复杂度降为O(m + n),大大降低了复杂度。
这篇作者博客将的特别详细易懂:(56条消息) 从头到尾彻底理解KMP(2014年8月22日版)_v_JULY_v的博客-CSDN博客_串的next数组怎么算
下面先直接给出KMP的算法流程:
1.如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++,继续匹配下一个字符;
2.如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]。此举意味着失配时,模式串P相对于文本串S向右移动了j - next [j] 位。
换言之,当匹配失败时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值(next 数组的求解会在下文的3.3.3节中详细阐述),即移动的实际位数为:j - next[j],且此值大于等于1。
next 数组各值的含义:代表当前字符之前的字符串中,有多大长度的相同前缀后缀。例如如果next [j] = k,代表j 之前的字符串中有最大长度为k 的相同前缀后缀。
KMP算法和暴力匹配相比,其实就是在回退时不用全部倒退回去,回退到我们所求的next数组的值即可,因为前面的部分仍然是相同的从而大大优化了复杂度,KMP算法最不好理解的地方就是next数组是什么以及如何求得的问题,下面给出解释,借鉴了上面的博主的博客内容,以及例题的next数组为y总总结的,不过字符串下标是从1开始的,理解后记忆即可。
①寻找前缀后缀最长公共元素长度
对于P = p0 p1 ...pj-1 pj,寻找模式串P中长度最大且相等的前缀和后缀。如果存在p0 p1 ...pk-1 pk = pj- k pj-k+1...pj-1 pj,那么在包含pj的模式串中有最大长度为k+1的相同前缀后缀。举个例子,如果给定的模式串为“abab”,那么它的各个子串的前缀后缀的公共元素的最大长度如下表格所示:
比如对于字符串aba来说,它有长度为1的相同前缀后缀a;而对于字符串abab来说,它有长度为2的相同前缀后缀ab。
②求next数组
next 数组考虑的是除当前字符外的最长相同前缀后缀,所以通过第①步骤求得各个前缀后缀的公共元素的最大长度后,只要稍作变形即可:将第①步骤中求得的值整体右移一位,然后初值赋为-1,如下表格所示:
比如对于aba来说,第3个字符a之前的字符串ab中有长度为0的相同前缀后缀,所以第3个字符a对应的next值为0;而对于abab来说,第4个字符b之前的字符串aba中有长度为1的相同前缀后缀a,所以第4个字符b对应的next值为1。
③根据next数组进行匹配
匹配失配,j = next [j],模式串向右移动的位数为:j - next[j]。换言之,当模式串的后缀pj-k pj-k+1, ..., pj-1 跟文本串si-k si-k+1, ..., si-1匹配成功,但pj 跟si匹配失败时,因为next[j] = k,相当于在不包含pj的模式串中有最大长度为k 的相同前缀后缀,即p0 p1 ...pk-1 = pj-k pj-k+1...pj-1,故令j = next[j],从而让模式串右移j - next[j] 位,使得模式串的前缀p0 p1, ..., pk-1对应着文本串 si-k si-k+1, ..., si-1,而后让pk 跟si 继续匹配。如下图所示:
给定一个字符串 S,以及一个模式串 P,所有字符串中只包含大小写英文字母以及阿拉伯数字。
模式串 P 在字符串 S 中多次作为子串出现。
求出模式串 P 在字符串 S 中所有出现的位置的起始下标。
输入格式
第一行输入整数 N,表示字符串 P 的长度。
第二行输入字符串 P。
第三行输入整数 M,表示字符串 S 的长度。
第四行输入字符串 S。
输出格式
共一行,输出所有出现位置的起始下标(下标从 0 开始计数),整数之间用空格隔开。
数据范围
1≤N≤1e5
1≤M≤1e6
输入样例:
3
aba
5
ababa
输出样例:
0 2
y总给出的模版是下标从1开始的,下标为0即表示重头开始匹配,next数组的计算过程类似于kmp的匹配过程,即求最长公共前缀后缀长度,然后全体向右移然后开头赋0的过程
这个模式串abcdabd是下标从0开始的,-1即表示为从头开始匹配
//优化过后的next 数组求法 void GetNextval(char* p, int next[]) { int pLen = strlen(p); next[0] = -1; int k = -1; int j = 0; while (j < pLen - 1) { //p[k]表示前缀,p[j]表示后缀 if (k == -1 || p[j] == p[k]) { ++j; ++k; //较之前next数组求法,改动在下面4行 if (p[j] != p[k]) next[j] = k; //之前只有这一行 else //因为不能出现p[j] = p[ next[j ]],所以当出现时需要继续递归,k = next[k] = next[next[k]] next[j] = next[k]; } else { k = next[k]; } } }
int KmpSearch(char* s, char* p) { int i = 0; int j = 0; int sLen = strlen(s); int pLen = strlen(p); while (i < sLen && j < pLen) { //①如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++ if (j == -1 || s[i] == p[j]) { i++; j++; } else { //②如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j] //next[j]即为j所对应的next值 j = next[j]; } } if (j == pLen) return i - j; else return -1; }
y总的模版可能不太好理解。还是例如abab 下标从1开始,i = 2,j = 0, p[i] != p[j+1],next[i
= 2] = j = 0, 然后i向后移动,p[i=3] == p[j+1], j = next[j] = 0, 然后j++,ne[i = 3] = 1, 然后 i = ,4, j = 1,p[i] == p[j+1], j = next[j = 1] = 0, ne[i = 4] = 0, 即0010
next[j]是不包含p[j]这个元素的前面的子字符串的最长公共前后缀长度
#include<iostream> using namespace std; const int N = 100010, M = 1000010; int n, m; char p[N], s[M]; int ne[N]; int main() { cin >> n >> p + 1 >> m >> s + 1; //next数组的实现 //从二开始 因为next[1] = 0 for(int i = 2, j = 0; i <= n; i++) { while(j && p[i] != p[j + 1]) j = ne[j]; //匹配失败回到最大重复的地方 if(p[i] == p[j + 1]) j++; ne[i] = j; //将当前子字符串p[0到i]的最长重复前后缀长度赋给ne[i] } //KMP匹配过程 for(int i = 1, j = 0; i <= m; i++) { while(j && s[i] != p[j + 1]) j = ne[j]; //如果不匹配就移动最小距离到最长重复前后缀的地方重新匹配 //或者j==0时字符串s与模式串p开头就不一样 s就向后走 if(s[i] == p[j + 1]) j++; if(j == n) { cout << i - n << " "; j = ne[j]; //匹配成功后返回到最小移动 也就是最大重复的前后缀的地方再检验是否还有匹配的 } } return 0; }