KMP算法自己的推演与总结

171 阅读4分钟

kmp算法是用于字符串匹配的快速算法。

假设我们要匹配的字符串为target,而待匹配的字符串为source。kmp的目标就是在小于O(n^2)的时间复杂度下完成字符串target在source的首次完全匹配。

假设如下情景:

字符串
sourcebababcde
targetbabc

当我们直到了target的长度后,很容易想到,用滑动窗口法,逐步地取出source中与target等长的字符子串,若不匹配,则窗口不断右滑。

但是上面的方法正如前面第一段所说,时间复杂度太高,而且在匹配过程中我们只用了target的长度这一个信息,而没有关注target字符串本身的特点。

仔细观察target,我们发现babc中出现了两个b,如果我们在匹配的过程中,bab这前三个字符已经匹配成功了,而第四个字符匹配失败,这个时候滑动窗口会怎么做?

答:滑动窗口的指针从原来指向b,变为指向source的下一个字符,然后继续从target的第一个开始重新匹配。

这样做的效率无疑是很低的!

实际上我们目前已知bab是已经匹配成功的,这说明source串中已经找到bab串,那么说明第二个b后面一个字符是匹配失败的,但是前面的字符都匹配成功了,如果target中存在重复的前缀,由于前面匹配成功了,因此只要以当前的元素为上一个重复前缀的位置,然后继续向后匹配则实现了对滑动窗口的剪枝。

例如,b在原target中的上一个前缀是b,因此我们直接从b开始匹配就可以了,而滑动窗口会从a开始匹配,KMP直接跨过了a为首元素匹配的情况

image.png 如何来找这个上一个重复元素的前缀位置呢? 我们首先来用一个指针i来表示当前正在遍历的target元素。再定义一个指针j,用来表示当前元素在上一次出现的前缀位置。

初始时刻,j=0,而i = 1 如果,target[j]= target[i],则匹配成功,当前的i和j可以作为前缀,i和j都向前移。 如果,target[j]!=target[i],则匹配不成功,让j回退到他出现的上一个前缀位置,继续判断与target[i]的相等情况,直到相等或者j=0(j=0则说明不存在前缀,从0开始重新匹配)。

我们用pi数组来存储每个位置元素的上一个前缀位置。

以上面的target babc为例

初始时刻:j = 0;i= 1; target[j]!=target[i],j=0,则pi[i] = j; 说明a不匹配的话,下一次得重新从0开始匹配

接着i+1后,i = 2。 此时,target[j]= target[i],j向后移动继续匹配, pi[i] = j = 1。说明如果b的下一个元素,不匹配成功的话,可以将当前的b当作是target[0]位置的b来考虑,然后从是target[0]位置的b的下一个位置,也就是第1个位置进行匹配

当i = 3时,target[i]=c,此时target[j=1]不等于c,然后继续返回1位置的上一个前缀,是0,则位置3的c不成功匹配的话,也会直接从0位置开始重新匹配target字符串。

所以 pi = [0,0,1,0];

下面是编程实现(思路就是寻找当前下标对应元素的在target中从头开始计数的上一个前缀位置):

public int strStr(char[] haystack,char[] needle){
   int n = needle.length;
   int[] pi = new int[n]
   //构建前缀位置数组pi
   for(int i = 1,j = 0;i<n;i++){
       while(j>0&&needle[i]!=needle[j]){
           j = pi[j-1];
       }
       if(needle[i]==needle[j]){
           j++;//j是匹配的字符,继续匹配需要从匹配字符的下一个字符开始算是否匹配
       }
       pi[i]=j;
   }
   for(int i = 0,j = 0;i<haystack.length;i++){
       while(j>0&&haystack[i]!=needle[j]){
           j = pi[j-1];
       }
       if(haystack[i]==needle[j]){
           j++;//j是匹配的字符,继续匹配需要从匹配字符的下一个字符开始算是否匹配
       }
       if(j==n){
           return i-n+1;//返回匹配位置;
       }
   }
   return -1;
}