一.KMP基础
1.算法目的:实现字符串匹配问题
2.算法原理:
在进行文本串和模式串的匹配过程中,若出现模式串与文本串不符的情况下,不必再回到初始位置重新开始匹配,而是回到不相等位置前的最长相等前后缀的位置继续进行匹配。
前缀:含首不含尾的连续子串
后缀:含尾不含首的连续子串
最长相等前后缀:
例: aabaaf
a 0 (既是首字符又是尾字符,所以长度为0)
aa 1 (第一个 a 是首,第二个 a 是尾,a = a,所以长度为1)
aab 0 (前缀可以是 a,aa;后缀可以是 b,ab;无相等字符串,所以长度为0)
aaba 0 (前缀可以是 a,aa,aab;后缀可以是 a,ba,aba;a = a,所以长度为1)
aabaa 2 (前缀可以是 a,aa,aab,aaba;后缀可以是 a,aa,baa,abaa;相等为 aa = aa,所以长度为2)
aabaaf 0
前缀表:用于存放所有字串的最长相等前后缀
拿上述例子来说,其前缀表就是:0 1 0 0 2
KMP的关键:在当前对文本串和模式串检索的过程中,若出现了不匹配,如何充分利用已经匹配的部分。
匹配到不正确的地方,模式串中,不正确地方之前的子串跟文本串的子串相等,所以模式串的子串的最长前缀肯定跟文本串最长子串的后缀有匹配
例如:文本串:aabaabaaf
模式串:aabaaf
匹配过程中先进性的是一一匹配,当 2 串与 1 串 匹配到 f 位置时,出现了不匹配的情况,那么就说明,2 串 f 位置前的子串(aabaa)与 1 串(aabaa) 对应子串是相等的,也就是说,2 串子串 的前缀中一定有与 1 串字串 后缀中 匹配的部分(就相当于是一个字符串,其前后缀一定有匹配部分,只不过最长相等前后缀的长度可能为 0 罢了,这时候也就是不匹配,这种情况下, 1 串 后面的部分也不会 与 2 串匹配成功了)
所以,我们记录 2 串所有子串的最长相等前后缀为前缀表,就是为了找到,2 串子串的前缀 与 1串 子串的后缀 匹配的位置,这样就可以从 相等的 1串 后缀的位置开始继续与 2串匹配,而不用再去从头开始一一匹配了。
前缀表怎么帮助我们找到该位置呢?前缀表记录的就是 2字符串 最长相等前后缀的长度,让指针直接跳到 冲突位置的前一位所对应的值 的位置继续寻找。就那上面的 aabaaf 例子匹配最长相等前后缀的例子来说,找其前一位的前缀表的值,就相当于去找子串 aabaa 的最长相等前后缀的值,而这个 aabaa 正好就是我们 1、2 串已经相等的部分,所以这种办法就可以直接找到是否两个字符串存在前后缀相等的情况以及这个前后缀相等的长度是多少。
f 之前这部分的字符串(也就是字符串aabaa)的最长相等的前缀 和 后缀字符串是 子字符串aa ,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面重新匹配就可以了。
总的来说,算法步骤就是,先求出模式串的所有子串的最长相等前后缀的长度,记录在前缀表中,然后与 文本串 进行一一匹配,当进行到不相等的位置时,查阅前缀表,找到冲突位置的前一位所对应的长度(
为什么要前一个字符的前缀表的数值呢,因为要找前面字符串的最长相同的前缀和后缀。
所以要看前一位的 前缀表的数值。)
跳转到该值的位置后,再与 文本串 进行一一匹配,直到匹配结束。
3.前缀表 next
1)不同形式
next 数组:当文本串与模式串某一位置不匹配时,模式串要退回的位置
不同人会有不同的写法,有些人会直接用求出来的值与对应子串位置形成 next表,此时再查找时,就是查找冲突位置的前一位的值
例:在上例 aabaaf 中,next = 0 1 0 0 2 0
有些人习惯将该值整体右移,此时在查找时,就直接查找冲突位置所对应的值
例:在上例 aabaaf中,next = -1 0 1 0 0 2
还有些人习惯整体 -1,在查找时还是查找冲突位置的前一位的值,然后将该值 +1
例:在上例 aabaaf 中,next = -1 0 -1 -1 1 -1
2)代码
这里就写第一种代码:
void getNxt(int * nxt, string &s) { //指向 nxt 数组的指针 和 要传入的字符串 s
j = 0;
nxt[0] = 0; // 初始化
for(int i = 1; i < s.size(); i++) {
while(j > 0 && s[i] != s[j]) {
j = nxt[j-1]; //前后缀不相等的情况下,退回
}
if(s[i] == s[j])
j++; //相等的情况下,j++,代表在之前匹配的最长长度之下再+1就是现在位置的长度
nxt[i] = j; //更新nxt数组的值
}
}
i:最长后缀的末尾,也是nxt数组的索引
j:最长前缀的末尾,也是 j-1 位字符串最长相等前后缀的长度
nxt[ i ] : 第 i 位 字符前面 i-1 位子串的最长相等前后缀的长度
(1)初始化
j = 0 ; j是从第 0 个位置开始匹配的,也代表了前面字符串的最长相等前后缀长度为0
nxt[0] = 0; 规定
i = 1; 因为 i、j 的定义,j = 0,i 就要从 1 的位置开始与之匹配
(2)前后缀不相等
前后缀不相等,j 就要退回
这里一定要使用 while ,因为是连续退回 ,直到找到相等的字符或者没找到到最前面为止。
j 一定是 > 0 ,而不能是 j >= 0 ,因为下面有 j-1 的操作,如果等于 0 会造成越界访问。
至于 退回的代码为什么是这样,建议看这个视频:
【KMP算法之求next数组代码讲解】 https://www.bilibili.com/video/BV16X4y137qw/?share_source=copy_web&vd_source=002438e9a1e7340e3b583fe31010c58a
直接看图解法就好了,这个视频的代码思路大致一样,写法不一样,因为视频中的字符串数组是从下标为 1 开始的,而我们习惯于 从0 开始。
(3)前后缀相等
相等的情况下,直接让 j ++,因为 j 代表的是 j-1 位字符串最长相等前后缀的长度,前后缀相等,说明这个长度还要再 +1.
注意在这个数组中,每次增加的数量最多为1.
(4)更新 nxt 数组
这一步一定要写在 if 语句的外面,因为不管是上面 前后缀不相等的情况还是相等的情况,由于 j 的特殊含义,nxt [i] = j 是不变的。
顶多就是,在不相等的情况下,j 退回到起始位置仍没有相等,那么此时 j = 0,说明相应的子串没有相等的前后缀,此时 nxt[i] = j = 0;相等的情况下就如上述所说,+1 直接赋值即可。
二.找出字符串中第一个匹配的下标
1.问题描述
实现 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() 定义相符。
2.思路分析
这道题就是利用 KMP 算法,先求出 needle 的 nxt 数组,然后让 haystack 与 needle 字符串进行一一匹配,遇到匹配错误的字符,查询 needle 的 nxt 数组,回退到相应位置,继续匹配,直到匹配结束。
代码:
class Solution {
public:
void getNxt(int * nxt,string &s) { //求nxt数组
int j = 0;
nxt[0] = 0;
for(int i = 1; i < s.size(); i++) {
while(j > 0 && s[j] != s[i]) {
j = nxt[j-1];
}
if(s[j] == s[i]) {
j++;
}
nxt[i] = j;
}
}
int strStr(string haystack, string needle) {
if(needle.size() == 0) { 第一种情况,needle 字符串长度为 0 ,不用匹配直接返回
return 0;
}
int nxt[needle.size()]; //创建 needle 的 nxt 数组
getNxt(nxt, needle); //获取 needle 的 nxt 数组
//开始两个字符串的一一匹配
int j = 0;
for(int i = 0; i < haystack.size(); i++) { //这里 i 一定要为 0,是从另一个字符
串的首部开始的,上面 i 为 1 是在
初始化一个字符串的后缀末尾位置,这
里的 i 就是haystack匹配字符的位置
//前后缀的匹配情况分析,和上面的大差不差
while(j > 0 && needle[j] != haystack[i]) {
j = nxt[j-1];
}
if(needle[j] == haystack[i]) {
j++;
}
// j 的长度 = needle,说明 haystack 中出现了 needle,直接返回
if (j == needle.size())
{
return(i - needle.size() + 1); //因为题目要求的是返回出现相等字符串的第一个
位置,i 就是现在指向的 haystack 字符串的
后缀位置,减去 needle 的长度后 +1 就是起
始字符的位置
}
}
return -1;
}
};
三.重复的子字符串
1.题目描述
给定一个非空的字符串 s
,检查是否可以通过由它的一个子串重复多次构成。
2.问题分析
(1)移动法
移动法的大致思路就是,将给定的字符串 s 进行拼接 得到 ss = s + s,去掉头尾,如果能在剩余部分中找到 s,就说明该字符串是由重复子串组成的。
例如:s = abcabc
我们进行拼接: s + s = abcabcabcabc
这时掐头掐尾 就是为了 破坏第一个和最后一个 字符串的完整性,避免我们在搜索的过程中搜索到原来这两个字符串。
代码如下:
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 ture;
return false;
}
注意:
1)begin()函数返回的是字符串的首元素位置
end() 函数返回的是字符串末尾元素的下一个元素的位置
所以这里一定要写成 end()-1 而不是 end(),写成end()就直接越界访问出错了。
2)std::string::npos
它用作字符串成员函数中长度参数的值,他作为返回值,它通常用于表示没有匹配项。
(2)KMP算法
这里有一个结论:
如果一个字符串是由其重复子串所组成的,那么该重复子串就是该字符串中不包含最长相等前后缀的部分。
推理过程可看:代码随想录
直接给代码:
//先求nxt数组
void getNxt(int *nxt, string &s) {
//初始化
int j = 0;
nxt[0] = 0;
for(int i = 1; i < s.size(); i++) {
//前后缀不相等,j回退
while(j > 0 && s[i] != s[j])
j = nxt[j-1];
//前后缀相等,j++
if(s[i] == s[j])
j++;
//更新
nxt[i] = j;
}
}
bool repeatedSubstringPattern(string s) {
if(s.size() == 0)
return false;
int nxt[s.size()];
getNxt(nxt, s);
int len = s.size();
if(nxt[len-1] != 0 && len%(len-nxt[len-1]) == 0)
return true;
return false;
}
注意:
(1)如果是重复子串数组,那么其最长相等前后缀的长度就被存放在 nxt 数组的最后一个位置,因为刚开始是无重复,最长相等前后缀就为0,之后重复的越多,前后缀自然也就重复的越多,其长度就会越长。
(2)如果是重复字串数组,不包含最长相等前后缀的部分就是他的重复子串,所以当字符串长度 去模 该部分,是能被整除的,不能整除就说明他不是重复子串。