28. 找出字符串中第一个匹配项的下标(KMP)

解题思路

KMP的经典思想就是:当出现字符串不匹配时,可以记录一部分之前已经匹配的文本的内容,利用这些信息避免从头再去做匹配。(代码随想录)

1、什么是KMP

KMP是由三位学者发明的,Knuth,Morrist,Pratt,取了三位学者名字的首字母。

2、KMP的作用

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

3、什么是前缀表

前缀表是用来回退的,它记录了模式串与文本串不匹配的时候,模式串应该从哪里重新匹配。
前缀表是:记录下标i之前(包括i)的字符串中,有多大长度相同前缀后缀
例:要在文本串aabaabaafa中查找是否出现过一个模式串aabaaf
如下图可以看出,文本串中的第六个字符b和模式串中第六个字符f不匹配了。28. 找出字符串中第一个匹配项的下标(KMP)_第1张图片

若使用前缀表,就看可以不用从头匹配。而是从上次已经匹配的内容开始匹配,找到模式串中第三个字符b继续开始匹配。

 28. 找出字符串中第一个匹配项的下标(KMP)_第2张图片

 

那么前缀表是如何记录的呢?
前缀表的任务是当前位置匹配失败,找到之前已经匹配的位置,在重新匹配,也就意味着在某个字符匹配失败,前缀表会告诉你下一步匹配,模式串应该跳到哪里。

4、最长公共前后缀。

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

5、为什么一定要用前缀表

回顾一下,刚刚在匹配过程中,在下标5的地方不匹配,如图

28. 找出字符串中第一个匹配项的下标(KMP)_第3张图片

 然后就找到了下标2,指向b,继续匹配,如图

28. 找出字符串中第一个匹配项的下标(KMP)_第4张图片

 

解释:下标5之前这部分的字符串(aabaa)的最长相等的前缀和后缀是子字符串aa,因为找到了最长相等的前缀和后缀,匹配失败的位置就是后缀字串的后面,那么我们就只需找到与其相同的前缀后面重新匹配就可以了。

6、如何计算前缀表

长度为前1个字符的子串a,最长相同前后缀的长度为0。
长度为前2个字符的子串aa,最长相同前后缀的长度为1。
长度为前3个字符的子串aab,最长相同前后缀的长度为0。
长度为前4个字符的子串aaba,最长相同前后缀的长度为1。
长度为前5个字符的子串aabaa,最长相同前后缀的长度为2。
长度为前6个字符的子串aabaaf,最长相同前后缀的长度为0。

28. 找出字符串中第一个匹配项的下标(KMP)_第5张图片

 可以看出,模式串与前缀表对于位置的数字表示的就是:下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。

28. 找出字符串中第一个匹配项的下标(KMP)_第6张图片

 找到的不匹配的位置,此时看它的前一个字符的前缀表数值是多少(因为要找前面字符串的最长相同的前缀和后缀),然后移动到下标为那个数值的位置

7、前缀表与next数组

next数组就是前缀表,很多实现是把前缀表统一减一,但也可以就是前缀表。

8、时间复杂度分析

文本串长度为n,模式串长度为m。因为在匹配过程中,根据前缀表不断调整匹配的位置,可以看出匹配的过程是O(n),之前还要单独生成next数组,时间复杂度是O(m),所以整个KMP算法的时间复杂度是O(m+n).暴力法的时间复杂度是O(m*n)。

9、构造next数组

//函数参数为指向next数组的指针,和一个字符串
void getNext(int* next, const string& s){
    int j = -1;//j指向前缀末尾的位置
    next[0] = j;
    for(int i = 1; i < s.size(); i++) { // 注意i从1开始
        while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了
            j = next[j]; // 向前回退,next[j]就是记录着j(包括j)之前的子串的相同前后缀的长度。那么 s[i] 与 s[j+1] 不相同,就要找 j+1前一个元素在next数组里的值(就是next[j])。

        }
        if (s[i] == s[j + 1]) { // 如果 s[i] 与 s[j + 1] 相同,那么就同时向后移动i 和j 说明找到了相同的前后缀,同时还要将j(前缀的长度)赋给next[i], 因为next[i]要记录相同前后缀的长度。
            j++;
        }
        next[i] = j; // 将j(前缀的长度)赋给next[i]
    }
}

10、使用next数组来做匹配

int j = -1; // 定义两个下标j 指向模式串起始位置,因为next数组里记录的起始位置为-1。
for (int i = 0; i < s.size(); i++) { //i指向文本串起始位置,注意i就从0开始遍历文本串
    while(j >= 0 && s[i] != t[j + 1]) { // 不匹配
        j = next[j]; // j 寻找之前匹配的位置,就要从next数组里寻找下一个匹配的位置
    }
    if (s[i] == t[j + 1]) { // 匹配,j和i同时向后移动
        j++; // i的增加在for循环里
    }
    if (j == (t.size() - 1) ) { // 文本串s里出现了模式串t
        return (i - t.size() + 1);
    }
}

代码

class Solution {
public:
    void getNext(int* next, const string& s)
    {
        int j = -1;
        next[0] = j;
        for(int i=1;i=0 && s[i]!=s[j+1])
            {
                j=next[j];
            }
            if(s[i] == s[j+1])
            {
                j++;
            }
            next[i]=j;
        }
    }

    int strStr(string haystack, string needle) {
        if(needle.size() == 0) return 0;
        int next[needle.size()];
        getNext(next,needle);
        int j=-1;
        for(int i=0;i=0 && haystack[i] != needle[j+1])
            {
                j=next[j];
            }
            if(haystack[i] == needle[j+1])
            {
                j++;
            }
            if(j == (needle.size()-1))
            {
                return (i-needle.size()+1);
            }
        }
return -1;
    }
};

你可能感兴趣的:(算法)