KMP算法小记

本文图片摘自代码随想录---KMP算法

什么是KMP?

KMP算法取自三位发明者的首字母,它主要应用在字符串匹配上。

KMP有什么用?

KMP的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。

那么什么是字符串匹配呢?比如说我们要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。如下图所示:

KMP算法小记_第1张图片

可以看出,文本串中第六个字符b 和 模式串的第六个字符f,不匹配了。如果暴力匹配,发现不匹配,此时就要从头匹配了。我们想要减少暴力匹配花费的时间,就要考虑如何记录已匹配的文本内容,于是我们引出了next数组。next数组又叫前缀表(prefix table),如果使用前缀表,就不会从头匹配,而是从上次已经匹配的内容开始匹配,找到了模式串中第三个字符b继续开始匹配。

关于前缀表

前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。它记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。

逐字剖析这句话,模式串与文本串在上文中已经提到,我们需要在文本串中找到与模式串相同的一段字符串。那么什么是前缀后缀呢?

前缀:不包含最后一个字符的所有以第一个字符开头的连续子串。

后缀:不包含第一个字符的所有以最后一个字符结尾的连续子串。

最长相同前后缀指的是在一个字符串中长度最长的相等的前缀与后缀。

"aabaaf" 
# 前缀包含 a;aa;aab;aaba,aabaa
# 后缀包含 f;af;aaf;baaf;abaaf;
# 所以它的最长相等前后缀长度为0
 
"a" #无前缀无后缀,最长相等前后缀长度为0
"aa" # 最长相等前后缀长度为1
"aab" # 最长相等前后缀长度为0
"aaba" # 最长相等前后缀长度为1
"aabaa" # 最长相等前后缀长度为2

所以字符串"aabaaf"的前缀表为[0, 1, 0, 1, 2, 0]。

为什么要用前缀表?

正如上文所说,前缀表记录了包括当前下标及其之前的字符串中的最长相同前后缀。当我们使用文本串来匹配模式串时,在某个下标处遇到不匹配,此时找到该下标的前一位下标所对应的前缀表中的值,从该值所对应的索引处开始匹配。为什么如此呢?

举个例子:

现在我们有文本串:"a a b a a b a a f" 和模式串"a a b a a f",由上文可知前缀表为[0, 1, 0, 1, 2, 0]。

文本串:    "a  a  b  a  a  b  a  a  f"
模式串:    "a  a  b  a  a  f"
前缀表:    [0  1  0  1  2  0]
// 已知在文本串和模式串的前5个字符均相等,均为"a a b a a"
// 当匹配到第6位时,出现了不匹配,此时对应 b 与 f
// 此时f的前一位对应的前缀表数值为2,说明在"a a b a a"的字符串中其最长相同前后缀为2,均为"a a"
// 我们现在在后缀"a a"的后面出现了不匹配,现在就要找与其相等的前缀的后面继续开始匹配,即模式串的b字符处
// 所以说我们可以通过看出现错误匹配的模式串的下标所对应的前一位下标的前缀表,来确认重新开始匹配的位置
// 因为前缀一定是从第一个字符开始,所以说前缀表对应的值一定是下一次匹配开始的下标

请注意,我们这里使用的"遇见不匹配看冲突点的前一位"是代码中的循环不变量,与之前博客二分法中提到的循环不变量概念相同,在之后的代码实现中会有体现。

如何代码实现前缀表?

前面我们已经通过手算计算出了"a a b a a f"字符串的前缀表,其为[0, 1, 0, 1, 2, 0]。值得一提的是,还有一些next数组的写法,如:[-1, 0, 1, 0, 1, 2]和[-1, 0, -1, 0, 1, -1]等。其实这并不涉及到KMP的原理,而是具体实现,next数组既可以就是前缀表,也可以是前缀表统一减一(右移一位,初始位置为-1)。例如第二种写法就是右移一位,初始位置为-1;第三种写法就是统一减一,这并不影响,只是在代码中书写的语句不同。要记住不管是什么样的前缀表,我们都是取前一位所对应的最大相同前后缀的长度作为重新开始匹配的索引下标。接下来均以不作任何改变的前缀表作为next数组。

代码实现前缀表:

  1. 初始化:由前缀表的定义可知,其对应的第一个元素一定为0。我们定义两个指针i和j,j指向前缀末尾位置,i指向后缀末尾位置。j的值不仅代表前缀的末尾位置,还表示了i之前(包括i)的字串的最长相同前后缀长度。前缀是从下标为零的位置开始,所以将j初始化为0;要比较前后缀字符是否相等,i不可能与j重合,所以将i初始化为1。从index = 0到index = i的位置就是当前需要求最大相等前后缀长度的字符串范围,我们可以利用i的遍历,输出next[i]中的对应的值。同时初始化next数组,next[0] = 0。

  1. 处理前后缀不相同的情况:即前后缀末尾不相同的情况,此时s[i] != s[j],前后缀末尾不匹配。

  i               *
模式串:    "a  a  b  a  a  f"
  j            *
前缀表:    [0  1  0  1  2  0]
// 此时i指向b,j指向a,根据上文分析,应该使j回退
// 此时j要回退到的下标就是它的前一位所对应的next数组中的值,即1
// 一直回退到s[i] == s[j]时或者不能再退了(达到模式串下标为0处时)回退结束,所以这里要用while()

其实这时候后缀串就相当于文本串了,一直往后遍历,不回头,前缀串相当于模式串,根据具体情况回退到合适位置

  1. 处理前后缀相同的情况:即前后缀末尾相同的情况,此时s[i] == s[j]。由于我们初始化j = 0,i = 1,此时如果前后缀末尾相同,则前后缀一定相同(因为从初始化刚开始就进入了循环判断)。

  i            *
模式串:    "a  a  b  a  a  f"
  j         *
前缀表:    [0  1  0  1  2  0]
// 此时i指向a,j指向a,根据上文分析,j的值不仅代表前缀的末尾位置,还表示了i之前(包括i)的字串的最长相同前后缀长度。此时i的字串为"a a",其最长相同前后缀长度为1,所以应该使j++,同时赋给next[i] = j

不管是前后缀相同还是不同的情况,在判断完并进行完相应的操作后都要进行i++并更新next。

代码实现

class Solution{
public:    
    // // int* next 和 int next[]在做形参时无区别
    void getNext(int* next, const string& s){    // 它既可以代表数组名,也可以代表单对象的指针
        int j = 0;
        next[0] = 0;
        for(int i = 0; i < s.size(); i++){
            while(j > 0 && s[i] != s[j]){
                j = next[j - 1];
            }
            if(s[i] == s[j]){
                j++;
            }
            next[i] = j;
        }
    }
};

总结

我们可以明显的看到,KMP算法不需要像暴力匹配那样,遇到不匹配就完全从头开始,这样的时间复杂度达到了O(n*m),其中n为文本串的长度,m为模式串的长度。KMP算法引入了next数组,在遇到不匹配时只需要找到冲突处的前一位所对应的next数组的值,并将其作为重新匹配的索引下标即可,其时间复杂度为O(m+n)(我们需要分别遍历一次两个字符串),其空间复杂度为O(m),为我们建立next数组所需要的空间大小。可见KMP算法满足工业上以空间换时间效率的基准

你可能感兴趣的:(c++,数据结构,算法,leetcode)