代码随想录算法训练营Day9|字符串KMP算法总结

代码随想录算法训练营

代码随想录算法训练营Day9字符串|KMP算法 8. 实现 strStr() 459.重复的子字符串 字符串总结 双指针回顾

28. 实现 strStr() KMP算法

题目链接:28. 实现 strStr()
给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回  -1 。
输入: haystack = "sadbutsad", needle = "sad"
输出: 0
解释:"sad" 在下标 0 和 6 处匹配。
第一个匹配项的下标是 0 ,所以返回 0 。

总体思路

因为KMP算法很难,大家别奢求 一次就把kmp全理解了,大家刚学KMP一定会有各种各样的疑问,先留着,别期望立刻啃明白,第一遍了解大概思路,二刷的时候,再看KMP会 好懂很多。

KMP算法

KMP算法的应用场景

KMP在进行字符串匹配时经常遇到,可以较为快速的匹配出所要查找的字符串

KMP算法定义

KMP算法通过比较最长相等前后缀得出前缀表进行判断是否有重复部分,从而减少对比时间

前后缀:

前缀是包含首字母,不包含尾字母的所有字串,如aabaac的所有前缀为a/aa/aab/aaba/aabaa五个前缀,后缀反之亦然。

最长相等前后缀

前缀或后缀中首位相同的字符数量,如a最长相等前后缀为0(单个字母首位相同,无最长相等前后缀),aa最长相等前后缀为1(首位均为a,有1个最长相等字符),aab最长相等前后缀为0(a!=b,因此无最长相等前后缀),aaba最长相等前后缀为1(前后均有a,该前缀的首位字符相等数为1),aabaa最长相等前后缀为2(该前缀有两个首尾字符相等,为2),aabaaf最长相等前后缀为0(首位无相等字符)。

前缀表

该字符串aabaaf的最长相等前后缀数量以此为:010120,故该字符串的前缀表为010120.

KMP算法的代码实现

next数组

核心思想:遇到了冲突的数组,向前回退,从而判断在何处进行重新的比对。
①初始化②前后缀不相同③前后缀相同④next
代码中 i 指向后缀末尾位置, j 指向前缀末尾位置
代码实现:

void getNext(next,s){//s为数组/
	 int j=0;next[0]=0;
	if(int i=1;i<s.size();i++){
		while(j>0&&s[i]!=s[j]){//回退是一个连续的过程,不是一次就好的
			j=next[j-1];
		}
		if(s[i]==s[j])
			j++;
		next[i]=j;
	}
}

本题目的代码实现:

class Solution {
public:
    void getNext(int* next, const string& s) {
        int j = 0;
        next[0] = 0;
        for(int i = 1; i < s.size(); i++) {
            while (j > 0 && s[i] != s[j]) {
                j = next[j - 1];
            }
            if (s[i] == s[j]) {
                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 = 0;
        for (int i = 0; i < haystack.size(); i++) {
            while(j > 0 && haystack[i] != needle[j]) {
                j = next[j - 1];
            }
            if (haystack[i] == needle[j]) {
                j++;
            }
            if (j == needle.size() ) {
                return (i - needle.size() + 1);
            }
        }
        return -1;
    }
};

459.重复的子字符串

题目链接:459.重复的子字符串
给定一个非空的字符串 s ,检查是否可以通过由它的一个子串重复多次构成。

总体思路

移动匹配

当一个字符串s:abcabc,内部由重复的子串组成,那么这个字符串的结构一定是这样的:

也就是由前后相同的子串组成。
那么既然前面有相同的子串,后面有相同的子串,用 s + s,这样组成的字符串中,后面的子串做前串,前后的子串做后串,就一定还能组成一个s,如图:

所以判断字符串s是否由重复子串组成,只要两个s拼接在一起,里面还出现一个s的话,就说明是由重复子串组成。
当然,我们在判断 s + s 拼接的字符串里是否出现一个s的的时候,要刨除 s + s 的首字符和尾字符,这样避免在s+s中搜索出原来的s,我们要搜索的是中间拼接出来的s。

class Solution {
public:
    bool repeatedSubstringPattern(string s) {
        string t = s + s;
        t.erase(t.begin()); t.erase(t.end() - 1); // 掐头去尾
        if (t.find(s) != std::string::npos) return true; // r
        return false;
    }
};

这种解法还有一个问题,就是 我们最终还是要判断 一个字符串(s + s)是否出现过 s 的过程,大家可能直接用contains,find 之类的库函数。 却忽略了实现这些函数的时间复杂度(暴力解法是m * n,一般库函数实现为 O(m + n))。
如果我们做过 28.实现strStr (opens new window)题目的话,其实就知道,实现一个 高效的算法来判断 一个字符串中是否出现另一个字符串是很复杂的,这里就涉及到了KMP算法。

KMP

代码实现:

class Solution {
public:
    void getNext (int* next, const string& s){
        next[0] = 0;
        int j = 0;
        for(int i = 1;i < s.size(); i++){
            while(j > 0 && s[i] != s[j]) {
                j = next[j - 1];
            }
            if(s[i] == s[j]) {
                j++;
            }
            next[i] = j;
        }
    }
    bool repeatedSubstringPattern (string s) {
        if (s.size() == 0) {
            return false;
        }
        int next[s.size()];
        getNext(next, s);
        int len = s.size();
        if (next[len - 1] != 0 && len % (len - (next[len - 1] )) == 0) {
            return true;
        }
        return false;
    }
};

字符串总结

字符串是数组的特殊组成形式,字符串是若干字符组成的有限序列,也可以理解为是字符数组。
在C语言中,把一段字符串存入一个数组时,也把"\0"存入数组,并作为结束标志

char a[5]="asd";
for(int i=0;a[i]!="\0";i++){}

在C++中,童工一个string类,string类会提供size接口,可以用来判断string类字符串是否结束,就不用“\0”来判断是否结束,例如

string a="asd";
for(int i=0;i<a.size();i++){}

vector<char>string在基本操作上没有区别,但string提供更多的字符串处理的相关接口,如string重载了+,而vector没有,所以处理字符串时最好定义string类型。

双指针回顾

双指针时十分常用的方法,在数组、字符串、链表中都有涉及。
很多数组填充类问题,都会预先给数组扩容带填充后的大小,然后再从后向前进行操作。
再for循环里调用库函数erase来移除元素,这是O(n²)的操作,因为erase是O(n)。

翻转系列

当需要固定规律一段一段去处理字符串的时候,要想想在for循环上做文章。
只要让i+=(2*k) ,i每次移动2k个单位就可以,然后判断是否又要反转的区间
151. 是先整体反转再局部反转,从而实现反转字符串里的单词

KMP算法

KMP的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。
KMP的精髓所在就是前缀表,在KMP精讲中提到了,什么是KMP,什么是前缀表,以及为什么要用前缀表。
前缀表:起始位置到下标i之前(包括i)的子串中,有多大长度的相同前缀后缀。
那么使用KMP可以解决两类经典问题:

  1. 匹配问题:28. 实现 strStr()
  2. 重复子串问题:459.重复的子字符串
    再一次强调了什么是前缀,什么是后缀,什么又是最长相等前后缀。
    前缀:指不包含最后一个字符的所有以第一个字符开头的连续子串。
    后缀:指不包含第一个字符的所有以最后一个字符结尾的连续子串。
    然后针对前缀表到底要不要减一,这其实是不同KMP实现的方式,我们在KMP精讲中针对之前两个问题,分别给出了两个不同版本的的KMP实现。
    其中主要理解j=next[x]这一步最为关键!
posted @ 2023-02-09 15:51  百里长川  阅读(32)  评论(0编辑  收藏  举报