数据结构—KMP模式匹配算法

[Data Structre] KMP

Posted by PYQ on April 20, 2022

Ⅰ. 字符串匹配

字符串匹配问题:字符串 P 是否为字符串 S 的子串?如果是,它出现在 S 的哪些位置? 其中 S 称为主串;P 称为模式串。C语言函数strstr()就是解决字符串匹配的函数,下图为Leetcode 28题,也是解决字符串匹配问题。

image-20220420095622896

如何去解决字符串匹配问题呢?

Ⅱ. Brute-Force (BF)

按照人脑的第一反应,进行字符串匹配时,从主串的第一个元素开始依次比较连续元素与模式串是否匹配,若出现不匹配则从主串的第二个元素开始,直到找到第一个匹配的位置或者找不到匹配的子串。上述解法也被称为Brute-Force (BF)解法,即暴力解法,也是我们常人能写出来的解法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
    int strStr(string haystack, string needle) {
        if(haystack.length() < needle.length())
            return -1;
        if(needle.length() == 0)
            return 0;
        for(int i=0;i<haystack.length()-needle.length()+1;i++){
            int j;
            for(j=0;j<needle.length();j++){
                if(haystack[i+j]!=needle[j])
                    break;
            }
            if(j==needle.length())
                return i;
        }
        return -1;
    }
};

PS:实际上使用BF算法已经可以在“LC-28”中AC了,甚至击败了100%的用户。。。。

BF算法理论上时间复杂度为O((m-n)*n)(也可近似为O(m * n),m, n表示主串和模式串的长度,实际过程中消耗的时间要比O((m-n) * n)小)。暴力解法很好理解,但若出现如下情况,这个解法就会变的“很傻”。

  • 主串:aaaaaaaaabc
  • 模式串:aaab

上述两串进行字符串匹配时,第一次在主串的第3个位置(从0开始)出现不匹配,BF算法的做法既是从主串的第1个位置再重新开始。此时按人脑的思维去看也觉得“很蠢”,因为我们可以一眼看出即使从第1个位置再重新开始也是做无用功,消耗多余的时间和空间,而我们的做法则是直接从第6个位置开始。这也就引出了KMP算法,KMP算法其实就是优化了在出现不匹配后下次开始匹配的位置。

人眼倒是可以直接看出下次开始的位置,那计算机怎么确定下次开始的位置呢?答案是next[]数组

Ⅲ. KMP

KMP算法的本质其实是动态规划问题,而next[]数组也和dp[]数组有相似性,next[]数组的精髓其实就是充分利用已经匹配的部分。

3.1 next[]

在理解next[]数组前,先要理解前缀、后缀和最长公共前缀。

  • 前缀:不包含最后一个字符的所有以第一个字符开头的连续子串。例如:对于字符串 abcxxxxefg,我们称 abc 属于 abcxxxxefg 的某个前缀。
  • 后缀:不包含第一个字符的所有以最后一个字符结尾的连续子串。例如:对于字符串 abcxxxxefg,我们称 efg 属于 abcxxxxefg 的某个后缀。

  • 最长公共前后缀长度:字符串中前k个字符恰等于后k个字符的最大的k。例如:“aba”中最长公共前后缀为1,“aabaa”中最长公共前后缀为2。

next数组是对于模式串而言的, next 数组就是一个前缀表,next[i]定义为下标为i的元素之前(==包括i==)的子串的最长公共前后缀长度

  • 主串:ababaabaabac
  • 模式串:abaabac

也有将next[]数组整体右移一位,即next[0]=-1,思想都是一样的,只是出现不匹配情况时选取next[]的下标不同

下标i 0 1 2 3 4 5 6
字符 a b a a b a c
next[i] 0 0 1 1 2 3 0

在清楚手算next[]后,如何使用代码去实现next[]的求解呢?

可分为如下步骤:

  1. 初始化
  2. 不匹配
  3. 匹配
  4. 更新next[]
1
2
3
4
5
6
7
8
9
10
11
12
13
void getNext(int *next, const string &s){
    //初始化定义j为前缀开始位置,i为后缀开始位置
    int j = 0;
    next[0] = 0;
    for(int i = 1; i < s.length(); i++){
        //j > 0是为保证j-1不越界,若不匹配则是找前一位的next[]数组,即回退
        while(j > 0 && s[i]!=s[j])
            j = next[j-1];
        if(s[i]==s[j])
            j++;
        next[i] = j;
    }
}

为什么在出现不等时进行回退,并且是找前一位元素的next[]值,这里引用知乎答主阮行止的回答

image-20220420135830883

3.2 字符串匹配

在求取next[]数组后,写出和理解字符串匹配的代码也就简单了,这里借用代码随想录的动图以便更好的理解如何利用next[]数组进行回退以及代码的书写

mmexport1650434981134

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int strStr(string haystack, string needle) {
    if(haystack.length() < needle.length())
        return -1;
    if(needle.length() == 0)
        return 0;
    int next[needle.length()];
    getNext(next, needle);
    int j=0;
    for(int i=0;i<haystack.length();i++){
        while(j>0;haystack[i]!=needle[j])
            j = next[j-1];
        if(haystack[i]==needle[j])
            j++;
        if(j == needle.length())
            return (i-needle.length()+1);
    }
    return -1;
}

其实可以发现strStr()函数有部分代码和getNext()函数类似,因为用到的思维都是一样的。

除了KMP算法外,还有BM算法以及改进的KMP算法(nextval[]数组),此处不进行记录。