KMP算法
用于解决字符串的模式匹配问题。它是求一个字符串(模式串)在另一个字符串(主串)中的位置。
首先最简单的一个方法就是暴力解法,通过一个循环,依次从主串中来匹配模式串。假设n、m分别表示主串和模式串的长度,那么按照暴力解法而言,最坏情况下的时间复杂度就是 O ( n ∗ m ) O(n*m) O(n∗m). KMP算法能够将这个暴力解法的时间复杂度降到线性的数量级。
假设目前我们有一个主串T和一个模式串P,如下所示:
主串和模式串分别有一个指针指向,分别是i和j:
按照暴力解法而言,接下来只需要同时移动i和j,然后比较其指向的字符是否一致即可。直到出现不一致的字符;
当出现不一样的字符之后,j则回退到模式串的第0位(假设下标是从0开始的),i则回退到主串的第1位,然后继续执行之前的过程。
假设n、m分别表示主串和模式串的长度,那么按照暴力解法而言,最坏情况下的时间复杂度就是 O ( n ∗ m ) O(n*m) O(n∗m).最坏的情况比如主串是“AAAAAB”,模式串是“AAB”.
KMP算法的出发点是:尽可能的利用已经部分匹配好的有效信息,保持i指针不进行回溯,通过修改j指针,让模式串尽可能的移动到有效位置,从而进行后续比对. 因此这就带来第一个问题,j指针的位置要如何计算呢?
下面我们先探讨一下j指针的移动规律:
如上图,此时i和j所对应的字符不匹配,那么j指针最好应该移动到哪个位置呢?之前的暴力解法需要i的回溯,因此j指针直接回退到0位置,但是我们为了更多的利用已经匹配好的信息,所以希望其能够回退到j=1的位置与i的位置对应,进行下一轮比对。为什么呢?因为此时有一个相同的子串A在i-1的位置和j=0的位置上对应了。
如下图也是同样的情况:
在这种情况下,我们最希望j回退到j=2的位置,然后与此时的i位置进行比对,这是因为此时前面有AB一一相同对应。
到此为止,我们仿佛已经找到了当匹配失败时,j指针要移动到的下一个位置k存在着这样的性质:P[0,k-1]=P[j-k,j-1].这个j表示的是发生匹配失败时模式串上对应的匹配失败的位置编号。 也就是上上图红色框的j的位置。可以通过下面这个图加以理解。
其实上面这个过程就是在找模式串的最长公共前后缀。其定义是:假设有一个串"p0 p1 p2 p3…pj",如果存在一个情况是“p0 p1 … pk = p_j-k … p_j”,那么我们就说这个串中有一个最大长度是k+1的最长公共前后缀。这里之所以要加一是因为下标是从0开始的,具体情况具体分析。
这就带来一个问题,如何来寻找前后缀呢?在找前缀时,要找除了最后一个字符的所有子串;在找后缀时,要找除了第一个字符的所有子串。
我们举个栗子:
以串abaabca为例,其对应的各个子串的最长公共前后缀统计如下所示:
此时最长公共前后缀长度就会和模式串P的每个字符产生一个对应关系,如下:
上述这个表的含义是,当以某个字符作为最后一个字符时,当前子串所拥有的最长公共前后缀的长度。
接下来引入next数组,next数组记录了模式串中每个字符位置上发生匹配失败时,下一次应该和主串对应位置进行比对的字符的下标。如模式串中在j 处的字符跟主串在i 处的字符匹配失配时,下一步用next [j] 处的字符继续跟主串i 处的字符匹配,相当于模式串向右移动 j - next[j] 位。
使用编程思想来递推求解next数组:这里要明确一点的是,next数组仅仅是针对模式串进行分析。
我们设next[0]=-1,为什么这样做呢?
这实际上对应着一种情况:当j为0的时候,此时就发生不匹配。如下图所示:
由于在第一个位置上就发生了匹配失误的问题,此时j是无法再向左移动,因此需要将主串i向右移动一位。这就是为什么将next[0]初始化为-1。
第二种情况:在第二个位置上发生匹配失误的问题,此时模式串左边只有一个位置A,此时j只能向左移动一个,因此next[1]=0。(解释一下next[j]的含义就是,当进行字符匹配的过程中,在模式串的第j的位置发生了匹配失误时,下次匹配时应该与主串i所对应的字符所在的下标。)
第三种情况,就是当pk=pj的时候,我们可以得出这样一个结论:next[j+1]=next[j]+1=k+1
第四种情况,更为普遍的一种情况,比如pk != pj时
此时k=next[k],为什么会有这样的结论呢?本人是这么理解的。以上图为例,假设在j的位置上发生了匹配失误,那么按照之前的说法,此时应该是next[j]=k,但是目前我们还有一个信息,那就是我们已经直到pk != pj,而kmp的思想就是尽可能的利用已知的匹配信息,所以如果依然按照之前的做法继续进行匹配的话,这一步是有些多余的。因此我们需要再找一个更短的公共前后缀,就像是下图所示的这个样子,此时就可以继续顺利的进行匹配了,同时此步操作显得并不多余。
KMP算法的时间复杂度为O(n+m),n是主串的长度,m是模式串的长度;
至此,next数组的建立过程就已经都解释清楚了。附相关代码:
暴力解法
int BF(SString T[],SString P[])
/**
* @description: 暴力匹配(朴素模式匹配BF)
* @param :
* @return {int}
*/
{
int i=0,j=0;
while (i < strlen(T) && j < strlen(P))
{
if (T[i] == P[j])
{
i++;
j++;
}else
{
i=i-j+1;
j=0;
}
}
if (j >= strlen(P))
{
printf("匹配成功.\n");
printf("模式串在主串中的编号地址为:%d\n",(i-strlen(P)));
return i-strlen(P);
}else
{
printf("匹配失败.\n");
return 0;
}
}
计算next数组
void GetNext(SString P[],int next[])
{
int j = 0, k = -1;
next[j] = k;
while (j < strlen(P))
{
if(k==-1 || P[k]==P[j])
{
j++;
k++;
next[j]=k;
}else{
k = next[k];
}
}
}
KMP算法:这里 特别要注意算法中的while循环的条件 ,这里添加了j==-1
这一条件。这是因为我们将next[0]设置为-1,如果不添加这个条件的话,那么在访问数组的过程中,就会访问到数组的-1处的位置,从而会导致算法提前结束,因此需要添加这一条件。当然,具体情况还需要具体分析。
int KMP(int start,char T[],char P[],int next[])
{
int i=start,j=0;
while(j == -1 || (i < strlen(T) && j < strlen(P)))
{
if(j==-1||T[i]==P[j])
{
i++; //继续对下一个字符比较
j++; //模式串向右滑动
}
else{
j=next[j];
}
// printf("i的变化:%d\n",i);
// printf("j的变化:%d\n",j);
}
if(j >= strlen(P)){
printf("匹配成功.\n");
printf("模式串在主串中的编号地址为:%d\n",(i-j));
return (i-j); //匹配成功返回下标
}
else{
printf("匹配失败.\n");
return -1; //匹配失败返回-1
}
}
主函数部分
#include <windows.h>
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "time.h"
using namespace std;
typedef char SString;
int main()
{
SString T[]="pktsinghusttjpuhit";
SString P[]="tsing";
// int index1 = BF(T,P); //暴力解法
int next[strlen(P)]; //定义next数组
GetNext(P,next); //计算next数组
// cout<<"计算得到的next数组元素为:"<<endl;
// for(int i = 0;i < strlen(P);i++){
// cout << next[i] <<" ";
// }
int index2 = KMP(0,T,P,next); //KMP方法
}
通过实际比较,发现越复杂的情况下KMP算法的时间优势越是明显,当然,有些简单的情况下,由于KMP算法还需要维护一个next数组,此时KMP算法的时间复杂度要比暴力解法的时间复杂度高。