代码随想录算法训练营Day9 | 28 实现 strStr()、459 重复的子字符串

本文介绍了如何使用KMP算法实现strStr函数,包括构建前缀表、处理前后缀不匹配和相同情况,以及应用KMP算法检测字符串是否由重复子串组成。作者分享了理解和学习KMP算法的过程和关键点。

28 实现 strStr()

  • 题意

    • 实现 strStr() 函数。
    • 给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。
      • 示例 1: 输入: haystack = "hello", needle = "ll" 输出: 2
      • 示例 2: 输入: haystack = "aaaaa", needle = "bba" 输出: -1
      • 说明: 当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。 对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与C语言的 strstr() 以及 Java的 indexOf() 定义相符。
  • 思路

    • KMP算法理论
      • 用途:解决字符串匹配问题
      • 前缀表:记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。
      • 最长公共前后缀:
        • 前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串。
        • 后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。
        • 前缀表要求的就是相同前后缀的长度。
        • 下标5之前这部分的字符串(也就是字符串aabaa)的最长相等的前缀 和 后缀字符串是 子字符串aa ,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面重新匹配就可以了。
      • 构造next数组:void getNext(int* next, const string& s)
        • 初始化:int j = 0;next[0] = j;
          • 两个指针i和j,j指向前缀末尾位置,i指向后缀末尾位置;
        • 处理前后缀不相同的情况:while (j > 0 && s[i] != s[j]) { j = next[j-1];
          • 前后缀不匹配的情况下,根据上一个j即j-1的前缀表退回到对应的位置;
        • 处理前后缀相同的情况:if (s[j] == s[i]) {j++;}
          • 前后缀匹配,则在已匹配的基础上再次+1
        • 更新: next[i] = j;
    • 主程序:
      • 计算needle即模式串的前缀表并存储;
      • 主串元素和子串元素不匹配,子串的指针根据前缀表回退;
      • 子串元素和主串元素相同,子串指针指向后一位;
      • 子串的指针指向子串的末尾,则找到子串;
  • 总结

    • 对于我来说,难点在于如何理解前后缀不相同的情况时,退回到上一个字符所对应的前缀表值;可以以ABACABAB来举例说明
    • 这里重点理解前ABA的前缀与后ABAB的后缀,因为ABA是相同的所以前后可以重合得到就是在ABAB中找相等前后缀,而ABA的最大相等前后缀为1,所以跳到第一个A进行下一次判断,即判断 j = 1时字符与尾字符是否相等,相等则j++,不相等回退直至j = 0;
    • 看不懂的话看看那个浅显易懂的KMP视频再模拟一下,链接如下
    • 最浅显易懂的 KMP 算法讲解_哔哩哔哩_bilibili
    • 帮你把KMP算法学个通透!(求next数组代码篇)_哔哩哔哩_bilibili
class Solution {
public:
    //获取字符串的前缀表
    void getnext(int* next, const string& s) {
        //初始化,j一开始就指向s的开头,并且第一个位置的前缀表值为0;
        int j = 0;
        next[j] = 0;

        // i为当前遍历的字母,开始为第二个字母,即i = 1;
        for (int i = 1; i < s.size(); i++) {
            //前后缀不匹配的情况下,根据之前的前缀表进行回退;
            // j回到了首位置,即j = 0时,不需要回退了
            while (j > 0 && s[j] != s[i]) {
                //当前不匹配,则找上一个字母的前缀表值回退到对应的位置
                j = next[j - 1];
            }

            //前后缀匹配,则在已匹配的基础上再次+1
            if (s[j] == s[i]) {
                j++;
            }

            //更新对应i的前缀表,即next[i];
            next[i] = j;
        }
    }
    int strStr(string haystack, string needle) {
        if (needle.size() == 0)
            return -1;
        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 重复的子字符串

  • 题意

    • 给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成。给定的字符串只含有小写英文字母,并且长度不超过10000。
    • 示例
      • 示例 1:
        • 输入: "abab"
        • 输出: True
        • 解释: 可由子字符串 "ab" 重复两次构成。
      • 示例 2:
        • 输入: "aba"
        • 输出: False
      • 示例 3:
        • 输入: "abcabcabcabc"
        • 输出: True
        • 解释: 可由子字符串 "abc" 重复四次构成。 (或者子字符串 "abcabc" 重复两次构成。)
  • 思路

    • 暴力解法
      • 将每个 (总长度 % 子串长度)为0的情况都循环检查
      • 检查的方式就是判断当前位置和前一个子串长度位置的字符是否相等;
	bool repeatedSubstringPattern(string s) {
		bool flag;
		int len = s.size();
		//i是子字符串的长度,即暴力查找每一个子字符串(子字符串的长度最多只能到len的一半)
		for (int i = 1; 2 * i <= len; i++) {
			//首先判断子串的长度能否被len整除
			if (len % i == 0) {
				flag = true;
				for (int j = i; j < len; j++) {
					//遍历主串的每一个字符,通过判断与前一个子串的对应位置是否相等来判断是否正确
					if (s[j] != s[j - i]) {
						flag = false;
						//出现不相等即为false,可以结束当前循环了
						break;
					}
				}
				//根据循环后的flag,来判断true还是false;
				if (flag) return true;
			}
		}
		//在遍历完1~1/2len的情况下,依然没有return true,即return false;
		return false;
	}
  • 移动匹配
    • 一个字符串s由重复的子串组成,则用两个s拼接起来的字符串ss,掐头去尾,中间仍然有一个s,即可说明s是由重复子串组成的;
class Solution {
public:
	bool repeatedSubstringPattern(string s) {
		string ss = s + s;
		//去头去尾
		ss.erase(ss.begin());
		ss.erase(ss.end() - 1);
		if (ss.find(s) != ss.npos) return true;
		return false;
	}
};
  • KMP算法
    • 数组长度减去最长相同前后缀的长度相当于是第一个周期的长度,也就是一个周期的长度,如果这个周期可以被整除,就说明整个数组就是这个周期的循环;
class Solution {
public:
	void getNext(int* next, const string& s) {
		int j = 0;
		next[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) {
		int* next = new int[s.size()];
		getNext(next, s);
		int len = s.size();
		//len - next[len-1]为字符串长度减去最长相等前后缀的长度,即为单个重复子串的长度,如果主串长度能整除子串长度,即为重复
		//主串最后一个字符的前缀表不能为零,否则len%len会满足条件,例如abac
		if (next[len-1]!=0 && len % (len - next[len - 1]) == 0) {
			return true;
		}
		return false;
	}
};

总结

        KMP是一块难啃的骨头,前前后后废了两三天的时间,当然,也有可能是我自己太摆烂了,毕竟今天才开始补这一块的博客。不过顺带复习了一遍,确实对KMP有了更进一步的认识,非常的巧妙啊,搞懂它还是挺有成就感的。

        目前,在二叉树中徘徊,加油,尽早结束二叉树,来写博客!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值