一文读懂 KMP 字符串查找算法

简介

  KMP 全称为:Knuth-Morris-Pratt,即为Knuth、Morris 和 Pratt 三人发明的算法,其基本思想是在文本串匹配中,当出现字符不匹配时,利用已匹配的模式字符串,避免从头再去做匹配,从而提高效率。 那KMP提高了多少效率呢?设n为文本串长度,m为模式串长度,则暴力匹配的时间复杂度为 O(n * m),而KMP只有 O(n + m)。

一些概念

  正式理解KMP算法前,先了解一些概念
  前辍:不包含最后一个字符的所有以第一个字符开头的连续子串。
  后缀:不包含第一个字符的所有以最后一个字符结尾的连续子串。
  最长相同前后辍:一个字符串中,所有相等的前辍与后辍中,最长的那一个。
  next[]:回退表,用于处理模式表回退。 它记录了文本串与模式串不匹配的时候,模式串应该从哪里开始重新匹配,由最长相等前后辍的长度计算得出。

为什么需要KMP?

  是不是很懵?上来一堆概念直接给整劝退了,上面这些概念,能记多少记多少,之后会有示例,示例解释过程中,需要反复回看以上概念。 请看以下示例:

  • 文本串为:ABAAAABAAAAAAAAA
  • 模式串为:BAAAAAAAAA
    • 前辍有:B、BA、BAA、BAAA、BAAAA、BAAAAA、BAAAAAA、BAAAAAAA、BAAAAAAAA
    • 后辍有:A、AA、AAA、AAAA、AAAAA、AAAAAA、AAAAAAA、AAAAAAAA、AAAAAAAAA
    • 最长相同前后辍:无(上述前辍和后辍中,无相同前后辍,更无最长相同前后辍)
  • i 为:文本串从0开始的索引
    一文读懂 KMP 字符串查找算法_第1张图片
      我们可以看到暴力匹配下做了很多无用功,我们文本串已经跑过i + 1个字符了,却还要不停重复 “再试” 之前跑过的字符,这时,能不重复跑吗?怎么做?
      能。我们跑过了i个字符,因为文本串第i + 1个字符与模式串第x个字符不匹配了,暴力匹配需要回退文本串的指针,此时,我们可以不回退文本串的指针,只回退模式串的指针,但怎样回退呢?这里把回退的索引存到数组next[]中,比如:模式串BAAAAAAAAA的回退表next[]值为:[0, 0, 0, 0, 0, 0, 0, 0, 0, 0](此数组索引为k),表示:当文本串第i + 1个字符与模式串第x个字符不匹配时,模式串需要回退到索引值next[k]处,这里回退表里元素next[k]全是0,即全部回退到模式串首部,直接用文本串第i + 1个字符与模式串索引值为0的字符(即第一个字符),继续比较即可。
      那么问题又来了,那为什么这么回退就可以了呢?这个就和前后辍有关了,前后辍相同时,可以在文本串第i + 1个字符与模式串第x个字符匹配失败时,告知模式串,前面有多少个字符不用重复“再试”,已经匹配成功了。 那么怎么计算回表next[]呢?请看下一节。

如何计算回退表next[]

计算方式如下:

  • next[] (回退表)长度与模式串长度相同。(毕竟为模式串服务)
  • 每个索引数值为:在模式串中,下标i之前(包括i)的字符串中,最长相同前缀后缀的长度。

“在模式串中,下标i之前(包括i)的字符串中,最长相同前缀后缀的长度。”
这句话多少有点难理解,所以这里做个示例:
设有模式串:AAABAA,当i = 2时,此时的字符串为AAA
前辍有:A、AA
后辍有:A、AA
最长相同前后辍为:AA,长度为2
故,当前 next[2] = 2
把所有的 next[] 填完为:[0, 1, 2, 0, 1, 2],即为回退表

代码实现为:

/**
 *  获取当前模式串的 next[] (回退表)
 *  这里没有右移一位,如果需要右移,所有值减1即可
 */
public void getNext(int[] next, String s){
		int j = 0;
        next[0] = 0;
        for(int i = 1; i < s.size(); i++) {
        	// j要保证大于0,因为下面有取j-1作为数组下标的操作
            while (j > 0 && s[i] != s[j]) { 
                // 注意这里,是要找前一位的对应的回退位置了
                j = next[j - 1]; 
            }
            if (s[i] == s[j]) {
                j++;
            }
            next[i] = j;
        }
    }

时间复杂度分析

   文本串长度为n,模式串长度为m。因为在匹配的过程中,虽然模式串根据回退表不断调整匹配的位置,但整个文本串每个字符,只会比较一次,故时间复杂度为:O(n)。此前还要单独计算next[](回退表),该算法也只遍历了一次,故时间复杂度是O(m)。所以整个KMP算法的时间复杂度是O(n+m)的。而暴力的解法显而易见是O(n × m),所以KMP在字符串匹配中极大的提高了搜索的效率。

实战

   接下来用KMP做一道leetcode上的 “简单题”
   28. 实现 strStr()
一文读懂 KMP 字符串查找算法_第2张图片
代码实现为:

/**
 * KMP算法,用KMP算法可以提高效率到O(n + m)
 */
class Solution {
	
	/**
 	*  获取当前模式串的 next[] (回退表)
 	*  这里没有右移一位,如果需要右移,所有值减1即可
 	*/
    public void getNext(int[] next, String s){
		int j = 0;
        next[0] = 0;
        for(int i = 1; i < s.length(); i++) {
        	// j要保证大于0,因为下面有取j-1作为数组下标的操作
            while (j > 0 && s.charAt(i) != s.charAt(j)) { 
                // 注意这里,是要找前一位的对应的回退位置了
                j = next[j - 1]; 
            }
            if (s.charAt(i) == s.charAt(j)) {
                j++;
            }
            next[i] = j;
        }
    }
    
    public int strStr(String haystack, String needle) {
        if(needle.length()==0){
            return 0;
        }

        int[] next = new int[needle.length()];
        getNext(next, needle);
        int j = 0;
        for(int i = 0; i < haystack.length(); i++){
            while(j>0 && haystack.charAt(i) != needle.charAt(j)){
                j = next[j - 1];
            }
            if(haystack.charAt(i) == needle.charAt(j)){
                j++;
            }
            if(j == needle.length() ){
                return (i - needle.length() + 1);
            }
        }

        return -1;
    }
}

你可能感兴趣的:(算法,算法,KMP,字符串)