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

KMP算法

kmp算法解决字符串匹配问题

思想核心:利用匹配失败后的信息,尽量减少模式串与主串的匹配次数

前缀与后缀

前缀:包含首字母、不包含尾字母的所有子串

               如aabaaf的前缀有:

                              a、aa、aab、aaba、aabaa(aabaaf不是前缀)

后缀:包含尾字母,不包含首字母的所有字串

               如aabaaf的后缀有:

                              f、af、aaf、baaf、abaaf(aabaaf不是后缀)

 最长相等前后缀——前缀表

从首字符开始,逐个判断子串的最长相等前后缀,存入数组,即为前缀表,如aabaaf的前缀表:

                       a:0(a既没有前缀也没有后缀)

                       aa:1

                       aab:0

                       aaba:1

                       aabaa:2

                       aabaaf:0

此时的前缀表:

                       子串:    a a b a a f

                       前缀表:0 1 0 1 2 0

使用前缀表进行字符串匹配

以 主串aabaabaaf 与 模式串aabaaf 的匹配过程为例:

        第一步——计算前缀表

                       如上文过程,计算得到模式串的前缀表

                       索引:           0 1 2 3 4 5

                       模式串:       a a b a a f

                       前缀表:       0 1 0 1 2 0

匹配开始:

        第二步——不断匹配直到发生不匹配或匹配完成:      

                       主串:          a a b a a b a a f

                       模式串:      a a b a a  f                    // 在f处发生冲突

        第三步——发生冲突时,根据前缀表跳转到下一个进行匹配的位置

       f处发生不匹配,查询其前一个字母的前缀表,查询结果为2,则跳到索引为2的位置继续进行匹配(移到最长相等前缀的下一位置,其索引刚好等于前缀表的值)

                       主串:           a a b a a b a a f

                       模式串:       a a b a a  f                    // f处发生冲突

                                                            ^

         移动模式串:

                        主串:           a a b a a b a a f

                        模式串:                a a b a a f

                                                             ^

                                // 将前缀移到后缀位置(前后缀相等,不需要再判断是否匹配)

                                // 从‘b’处重新开始匹配(最长相等前缀的下一位置,其索引值为2)

        第四步——不断循环第二第三步直到匹配结束

next数组

遇到不匹配时,next数组告诉我们该回退到哪个位置(即前缀表的具体实现)

next数组的几种实现方式:

                        前缀表:       0 1 0 1 2 0

                        模式串:       a a b a a f

                        next数组:    0 1 0 1 2 0           

                                        // 与前缀表相同,查询冲突元素的前一个元素

                       前缀表:       0 1 0 1 2 0

                       模式串:       a a b a a f

                       next数组:   -1 0 1 0 1 2           

                                        // 前缀表整体右移一位,首字母为-1,直接查询冲突元素

                       前缀表:       0  1  0  1  2  0

                       模式串:       a  a  b  a  a  f

                       next数组:   -1  0 -1  0  1  -1            // 前缀表整体-1

求next数组的代码实现步骤

相当于模式串自己与自己匹配(后缀与前缀匹配),通过递推的方式求出next数组

实现——双指针法(基于next数组的第一种实现方式

       i:遍历子串(不断向后移,不发生回退)

       j:寻找该i值下的最长相等前后缀(冲突时利用next数组不断回退寻找最长相等前后缀)

一、初始化

i:指向后缀末尾,初始化为1(i != j,且首字母不需要判断)

j:指向前缀末尾,初始化为0(前缀末尾也代表着此时最长相等前后缀的长度)

next[0]初始化为0(首字母不需要判断,从第二个字符开始判断)

二、处理前后缀不相同的情况

       j回退到next[j-1](遇到冲突看前一个元素的前缀表)

       回退条件:j > 0(j = 0说明已经到了首字母,无法继续回退了)且s[i] != s[j](发生冲突)

// 如果发生冲突且j还能回退的话则继续回退
// 如果j回退到首字母,且首字母也冲突时退出回退循环
while( j > 0 && s[i] != s[j])
    j = next[j - 1];

三、处理前后缀相同的情况    

// 匹配成功则j后移,此时j之前的子串即为最长相等前缀
// j的值即为该最长相等前缀的长度
j++;                                      
next[i] = j;    // 更新i处的next数组值
i++;            // 前移i

四、更新next数组的值

       在前后缀相等情况时完成

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

KMP算法的代码实现

分为两个部分:

        1、求模式串的next数组

        2、使用next数组进行模式串与主串的匹配

vector getNext(string s) {
	// 主要注意next[0]初始化为0(第一个字符不存在前后缀,next一定为0)
	vector next(s.size(), 0);
	// j指向前缀末尾,初始化为0(从首个字符开始判断)
	int j = 0;
	
	// i指向后缀末尾,初始化为1(第一个字符不需要判断,从第二个字符开始比较子串的前后缀)
	for (int i = 1; i < s.size(); ++i) {
		// 处理前后缀不相同的情况
		// 如果发生冲突且j还能回退的话则继续回退
		// 如果j已经回退到首字母,且首字母也冲突时退出回退循环
		while (j > 0 && s[i] != s[j]) {
			j = next[j-1];
		}
		// 处理前后缀相同的情况
		// 匹配成功则j后移,此时j之前的子串即为最长相等前缀,将其赋值给next[i]
		// ++i(进入下一次循环自动实现)
		if (s[i] == s[j]) {
			next[i] = ++j;
		}
	}
	return  next;
}


// 思路与获取next数组差不多(获取next数组的过程相当于模式串的后缀与前缀匹配)
int strStr(string haystack, string needle) {
	vector next = getNext(needle);
	int j = 0;

	for (int i = 0; i < haystack.size(); ++i) {
		// 发生冲突时j进行回退(这步的目的是利用匹配失败后的信息,减少下一次匹配的工作量)
		while (j > 0 && haystack[i] != needle[j])
			j = next[j - 1];
		// 匹配成功时j后移
		if (haystack[i] == needle[j]) 
			++j;
		// 如果此时j等于模式串长度说明找到了模式串在子串内的位置,进行返回
		if (j == needle.size()) 
			return i - needle.size() + 1;
	}
	return -1;
}

459. 重复的子字符串

初看总觉得有点思路但又差一步,想了半天没想出来,看了视频解析才一下子恍然大悟。

KMP思路: 如果主串能够由重复子字符串组成,那么最长相等前后缀中不包含的那部分就是最小重复单元(需要一定数学推理)

实现:求主串长度与最长相等前后缀长度的差值,如果该差值能被主串长度整除则说明成立

bool repeatedSubstringPattern(string s) {
	int n = s.size();
	vector next(n, 0);
	int j = 0;

	// 求next数组,最后一个元素的next值就是最长相等前后缀长度
	for (int i = 1; i < n; ++i) {
		while (j > 0 && s[i] != s[j])
			j = next[j - 1];
		if (s[i] == s[j])
			next[i] = ++j;
	}
	int ans = n - next[n - 1];

	// 两个条件
	return ans < n && n % ans == 0;
}

你可能感兴趣的:(数据结构)