一、引言,何为KMP算法
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)
下面开始介绍KMP算法,在此先规定好文章中所用变量代表意义:
字符串A为被查找对象,字符串B为查找对象,即在A中寻找B是否存在
数组 next 为字符串B经过KMP算法之后得到的用于记录指针移动的数组
设字符串A的长度为m,字符串B的长度为n
用#代表一个为止的字符
k为头字符串指针,j为尾字符串指针
二、暴力算法
遇到字符串匹配问题(在字符串A中查找字符串B是否存在),很多初学者第一时间想到的就是遍历整个字符串A。当遇到A、B首字母相同的时候开始遍历字符串B判断是否相同,若不相同则字符串B向后移动,直到遇到下一次A、B首字母相同再次开始遍历,最后发现相同的时候退出。
乍一看,对于上图两个字符串A、B,似乎暴力算法也不算很麻烦,但是请看下图的两个字符串
对于字符串B,每次需要遍历完整个字符串才会发现最后一个不相同,于是字符串B向后挪动一位继续重新开始比较,于是可以得出整个算法时间复杂度大概为O(m * n),当m、n非常大时间可以看出我们浪费了很多不必要的时间
三、过渡(思路)
不难发现对于图2中的两个字符串,浪费我们时间的主要因素有两点:
1.对于A中的每一个字母都可以和B的首字母匹配,于是每个A中的字符都可以作为B的起点,总共 需要比较m次。
2.对于B中的首字母之后的字母恰好可以和A中充当首字母的之后的字母匹配上。
因素一对应时间负责度O(m* n)中的m,而因素二则对应n
于是得到这样一个思路,既然字符串B前部分aaa相同,只有最后的b是不匹配的,那么我们是不是可以把字符串B向后移动一个字符,然后从第三个字符开始比较呢?即字符串A中全部是a,我们只需要在其中找到一个字符b即可判断字符串B存在,如下图:
这样一来便不再需要比较 m * n 次,我们只需要判断字符串B向后移动一位之后,A中的第四个字符能否和B中的第三个字符匹配。若能,则将A之前的第四位字符看做当前的第三位字符,向后寻找新的第四位字符。
更通俗的解释就是:
当前字符串A是 aaa#1#2,已知#1不是b,那么若#1是a,则字符串A变成功aaaa#2,此时去掉第一个a,即(a)aaa#2,现在只需要判断#2是不是b,若是则B存在,若不是则重复上述步骤
于是整体思路已确定,接下来的问题是如何判断数组B的指针在不匹配之后应该指向哪里,这个过程只需要字符串B,即每个字符串B有且仅有唯一对应的数组next。
先继续从图3的字符串B入手,得出数组B的核心在于,判断最长相同头字符串、尾字符串,过程如下:
当指针指向第三个字符的时候判断第二个字符前面的部分“a1a2”,最长相同头字符串、尾字符串是“a1”和“a2”,即当第三个字符不匹配的时候,返回到第一个字符之后(为什么没有判断前两个字符的原因稍后会解释)
当指针指向第四个字符的时候判断第二个字符前面的部分“a1a2a3”,最长相同头字符串、尾字符串是“a1a2”和“a2a3”,即当第三个字符不匹配的时候,返回到第二个字符之后
为了实现上述过程,我们需要两个指针,一个为最长相同头字符串,一个为最长相同尾字符串,当两个指针指向的字符相同的时候同时向后移动,并记录下之前相同的长度,整体过程如下
值得注意的是,每次判断的过程是,先判断->再移动->移动之后记录,而不是先移动->再判断->再记录,对于上述next数组的意义即,判断B指向的字符和A指向的字符不同的时候,B的指针,返回到对应next记录的下标处。如最后一个字符b不同,那么B的指针指向B[next[3]],即B[2],即a3
而考虑到不是所有B都是k,j都是同时移动的,可能出现k,j指向字符不相等情况,这个时候只有一个指针可以移动,于是考虑到先判断再移动的顺序,将next[0]设为-1,k,j不匹配的时候k = next[k],如此可以防止头字符串中也存在相同头字符串和尾字符串,如图6
以下列举一个k会左移的字符串B求next数组的过程:
四、具体代码实现
整体思路过程已经确定好了,接下来就是代码的实现,在此以函数的形式写出对next数组赋值的操作。
void GetNext(char* t, int* next) //t为字符串B,next为创建的数组next
{
int j, k, str = strlen(t); //k为头字符串指针,j为尾字符串指针
j = 0; k = -1; //首先初始化K为-1,因为判断过程先先判断在移动
next[0] = -1; //next[0]为-1是为了之后头字符串的移动
while (j < str - 1)
{
if (k == -1 || t[j] == t[k]) //先判断
{
j++; k++; //再移动
next[j] = k; //再记录,k指针指向的位置就是AB字符不匹配的时候B指针应当返
//回的位置
}
else
{
k = next[k]; //这一步很关键,表明了不匹配的时候k指针应当回到的位置
//具体可以参考图5和图6
}
}
}
在此附上力扣题目的链接,感兴趣可以去尝试以KMP写法提交:https://leetcode-cn.com/problems/implement-strstr/