代码随想录算法训练营第九天|28. 找出字符串中第一个匹配项的下标、459. 重复的子字符串

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

链接:28. 找出字符串中第一个匹配项的下标

思路:

KMP为匹配字符串最重要的算法之一,理解起来也比较有难度,要点为理解前缀表的含义。前缀表由一个和模式串相等长度的数组组成,其下标代表了前后缀相同的最长的字符串的长度

前缀:所有从第一个字符开始,不包括最后一个字符的连续子字符串。

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

如字符串abcabd,如果要填充这个字符串的前缀表,我们要进行如下操作。

首先考虑子字符串a,因为只有第一个字符,前缀为a,后缀不存在,所以最长相同前后缀长度为0。

然后考虑子字符串ab,前缀为a,后缀为b,最长相同前后缀长度也为0。

子字符串abc, 前缀为a、ab,后缀为b、bc,最长相同前后缀长度还是0(注意位置必须对应上,如‘b'虽然重复出现,但是在前缀为第二个字符,后缀为第一个字符,这样也不行)。

子字符串abca,前缀为a、ab、abc,后缀为a、ca、bca,我们发现相同的前后缀为a,所以最长相同前后缀长度为1。

子字符串abcab,前缀为a、ab、abc、abca,后缀为b、ab、cab、bcab,我们发现相同的前后缀为ab,所以最长相同前后缀长度为2。

子字符串abcabd,前缀为a、ab、abc、abca、abcab,后缀为d、bd、abd、cabd、bcabd,这个时候没有相同的前后缀,所以最长相同前后缀长度为0。

综上所述,字符串abcabd的前缀表为[0, 0, 1, 2, 0]。有些kmp的实现会把前缀表的所有元素减1,其实都是可以的,只是具体实现稍微不一样。我们可以用一个函数getNext()来实现前缀表。这里以前缀表都减1为例,首先初始化j和next数组,因为都减了1,所以前缀表的第一个元素为-1。

        int j = -1;
        // 前缀表的第一个数为-1
        next[0] = j;

然后我们需要两个指针i和j,分别代表后缀的第一个字符和前缀的第一个字符。然后用一个for循环遍历模式串,注意i初始化为1,因为后缀是不包含第一个字符的。每次循环,我们都要考虑s[i]和s[j+1]的匹配情况(这里要j+1因为j之前减了1),如果不匹配,就要不断退回到上一个匹配的地方,或者退回到next[0]的位置。

            while (s[i] != s[j+1] && j >= 0) 
                j = next[j];

如果相匹配,就增加j,最后再更新next的值。

            if (s[i] == s[j+1])
                j++;
            // 更新next数组
            next[i] = j;

通过以上步骤完成前缀表后,便可以用前缀表快速匹配文本串和模式串。遍历文本串,每次匹配的过程中,如果文本串和模式串有不匹配的字符,按照同样的方式退回j,直到完整匹配模式串的字符,或者i走到文本串终点未找到匹配模式串的字符。

代码:

class Solution {
public:
    int strStr(string haystack, string needle) {
        int next[needle.size()];
        getNext(next, needle);
        // 从-1开始
        int j = -1;
        for (int i = 0; i < haystack.size(); i++)
        {
            // 如果文本串没有匹配模式串,j就从前缀表里退回一个位置
            while (haystack[i] != needle[j+1] && j >= 0)
                j = next[j];
            // 如果匹配了的话,i和j同时向后移动
            if (haystack[i] == needle[j+1])
                j++;
            if (j == needle.size() - 1)
                return i - needle.size() + 1;
        }
        return -1;
    }
    void getNext(int* next, const string s)
    {
        // 统一减1的前缀表
        int j = -1;
        // 前缀表的第一个数为-1
        next[0] = j;

        // 注意i从1开始,因为后缀不包括第一个字符
        for (int i = 1; i < s.size(); i++)
        {
            // i表示后缀的第一个字符
            // j表示前缀的第一个字符
            // 如果前后缀不相同,j根据next数组上的值退回
            while (s[i] != s[j+1] && j >= 0) 
                j = next[j];
            // 如果前后缀相同,更新j
            if (s[i] == s[j+1])
                j++;
            // 更新next数组
            next[i] = j;
        }
    }
};

时间复杂度:O(m+n)

空间复杂度:O(m)

其中m是模式串的长度,n是文本串的长度

LeetCode 459. 重复的子字符串

链接:459. 重复的子字符串

思路:

这道题也可以用KMP来解决。通过getNext找到前缀表后,我们可以发现如果这个字符串是由重复的子字符串构成的,那么最长相同前后缀长度在经过第一个子串后,一定是增加的。如字符串abcdeabcdeabcde,其前缀为[0,0,0,0,0,1,2,3,4,5,6,7,8,9,10]。这是因为在重复的子字符串下,后缀一定可以和前缀匹配上。我们可以通过next[s.size()-1]找到最长相同前后缀长度,在这里为10,然后用字符串的长度减去10,可得重复的字串的长度15-10=5。这时再用字符串的长度模除子串的长度,如果能被字串整除,则代表字符串是由重复的子串组成的。

代码:

class Solution {
public:
    bool repeatedSubstringPattern(string s) {
        int next[s.size()];
        getNext(next, s);
        if (next[s.size() - 1] != 0 && s.size() % (s.size() - next[s.size() - 1]) == 0)
            return true;
        return false;
    }
    void getNext(int *next, const string s)
    {
        // 前缀表不统一减1
        int j = 0;
        next[0] = j;
        for (int i = 1; i < s.size(); i++)
        {
            while (s[i] != s[j] && j >0)
                j = next[j-1]; //因为要退回到前一个所以要减1

            if (s[i] == s[j])
                j++;
            // 更新前缀表
            next[i] = j;
        }
    }
};

时间复杂度:O(m+n)

空间复杂度:O(m)

其中m是模式串的长度,n是文本串的长度

你可能感兴趣的:(代码随想录算法训练营,leetcode,算法)