KMP算法
Knuth-Morris-Pratt算法(简称KMP),是一种高效的字符串匹配算法
相较于暴力匹配O(mn)的时间复杂度,KMP的算法时间复杂度为O(m+n),性能上有极大的提升,KMP算法极大的减少了不必要的回溯过程。
假定搜寻的文本串下标索引是i,模式串下标索引是j,KMP算法在匹配过程中,文本串下标i会一直递增,直到结束,不会回退。
一.NEXT数组应用
NEXT数组是KMP算法的核心,它指向当文本与模式串失配时模式串将要回退到的位置。
假定模式字符串为 aabaab,那么这个模式串的NEXT是(暂且不管怎么获得的){-1,0,1,0,1,2}

当模式串在第0位发生失配,文本指针i向后移动一位
当模式串在第1位发生失配,模式串指针回退到第0位
当模式串在第2位发生失配,模式串指针回退到第1位
当模式串在第3位发生失配,模式串指针回退到第0位
当模式串在第4位发生失配,模式串指针回退到第1位
当模式串在第5位发生失配,模式串指针回退到第2位
例:寻找字符串‘aababaabaaba’中’aabaab’首次出现的起始位置,那么匹配过程是这样的:




当模式串在第4位发生失配,模式串指针回退到第1位


当模式串在第1位发生失配,模式串指针回退到第0位


当模式串在第0位发生失配,文本指针i向后移动一位






匹配完成,获取’aabaab’首次出现的起始位置为5

二.制取NEXT数组
重新观察一下模式串’aabaab’的NEXT数组
当j=5的位置发生失配时,next数组指示模式串指针j回退到2位置
观察j=5位置,可以发现
substr[j-1] = substr[next[j-1]] => substr[4] = substr[next[4]] => substr[4] = substr[1]
substr[j-2] = substr[next[j-2]] => substr[3] = substr[next[3]] => substr[3] = substr[0]
也就是说 j = 5的前两个元素与next[5]所指向的2的前两个元素相等
正因为这样,在j=5位置发生失配时,j指针才能直接回退到2
例如用这个模式串去搜索aabaacaabaab

在j=5的位置发生失配,j指针回退到2

回退后我们可以保证模式串j=0,j=1位置的字符必定与文本i=3,i=4位置的字符相等,所以只需判断移动后模式串j位置的字符与文本i位置的字符是否相等即可
我们取得‘aabaab’的所有前缀,无视每个前缀的最后一个元素,每个前缀除去最后一个元素,后缀与前缀的重叠次数就是next的值,第一个next值固定为-1,第二个next值固定为0
S NEXT
a -1(固定)
aa 0(固定)
aab 1(aa的前后缀有 a、a)(相同的有一个)
aaba 0(aab的前后缀有a、aa、b、ab)(没有相同的)
aabaa 1(aaba的前后缀有a、aa、aab、a、ba、aba)(相同的有一个)
aabaab 2(aabaa的前后缀有a、aa、aab、aaba、a、aa、baa、abaa)(相同的有两个)
这样,我们就求得了NEXT数组={-1, 0, 1, 0, 1, 2}
再次观察

可知,如果我们想知道next[j+1]的值,只需判断T[j]是否等于T[ next[j] ]
1.我们可以定义一个临时变量:k = next[j]
2.如果k = -1,那么next[j + 1] = 0,否则继续步骤3
3.如果T[j] == T[k],那么next[j+1] = k + 1,否则继续步骤4
4.如果T[j] != T[k],那么k=next[k],重复判断步骤2
例:J表示模式串索引,T表示模式串数组,N表示模式串NEXT数组
一、j = 2, N[j]=?

假定 k = N[j – 1] = N[1] = 0
判断 T[j – 1] 是否等于 T[k]:T[j-1] = T[1] 是否等于 T[k] = T[0]
T[1] = T[0] => ‘a’ 等于 ‘a’
因为T[1] = T[0],所以N[j] = k + 1 => N[2] = 1

二、j = 3, N[j]=?
假定 k = N[j – 1] = N[2] = 1
判断 T[j – 1] 是否等于 T[k]:T[j-1] = T[2] 是否等于 T[k] = T[1]
T[2] = T[1] => ‘b’ 不等于 ‘a’
所以k = N[k] = N[1] = 0
再次判断 T[j – 1] 是否等于 T[k],T[2] = T[0] => ‘b’ 不等于 ‘a’
所以k = N[k] = N[0] = -1
因为k = -1,所以N[3] = 0

制取Next数组代码:
size_t npos = static_cast<size_t>(-1);
sizr_t* KMP_MakeNext(const char* substr, size_t len)
{
size_t* next = new size_t[len];//next数组是new的,没使用智能指针,所以用完注意释放
next[0] = npos;
int j = 0;
int k = npos;
while(j < len - 1)
{
if(k == npos || substr[j] == substr[k])
{
++j;
if(k == npos) k = 0;
else ++k;
next[j] = k;
}
else
{
k = next[k];
}
}
return next;
}
四.字符串匹配
既然已经获得了Next数组,那么字符串匹配就是一件小事了。
以下便是使用NEXT进行字符串匹配的代码
size_t KMP_Find_Count(const char* findstr, const char* substr)
{
size_t find_len = strlen(findstr), subs_len = strlen(substr);
if(subs_len == 0 || find_len < subs_len) return 0;
size_t* next = KMP_MakeNext(substr, subs_len, false);
size_t count = 0;
size_t j = 0, k = 0;
while( j < find_len )
{
if(k == npos || findstr[j] == substr[k])
{
//指示后移,或匹配成功
++j;
if(k == npos)
{
k = 0;
}
else
{
++k;
}
if(k == subs_len)
{
//子串匹配成功,计数+1,并回溯到匹配成功的位置,继续匹配
++count;
k = next[k - 1];
--j;
}
}
else
{
k = next[k];
}
}
delete[] next;
return count;
}