、
📝个人主页:@Sherry的成长之路
🏠学习社区:Sherry的成长之路(个人社区)
📖专栏链接:练题
🎯长路漫漫浩浩,万事皆有期待
文章目录
- kmp算法
- 找出字符串中第一个匹配项的下标
- 重复的子字符串
- 移动匹配
- kmp算法
- 总结:
kmp算法
kmp算法是一种快速查找字符串的算法,主要用途是在一个字符串里查找是否包含另一个字符串,kmp在理解上不是很友好,首先我们要搞清楚什么是前后缀、最长相等前后缀、前缀表、next数组如何实现前缀表,最后就是kmp算法借助前缀表的实现。
前后缀的概念
前缀就是包含第一个字符,而不包含最后一个字符的所有字串都可以被称为前缀。
后缀是包含最后一个字符,且不包含第一个字符的所有字串都可以被称为后缀。
举个例子:hello 前缀可以是h,he,hel,hell 后缀可以是:e,el,ell,ello
最长相等前后缀
最长相等前后缀顾名思义,就是前后缀相等的情况下,我们取最长的那一段。
前缀表,next数组如何实现?
前缀表十分重要,它是KMP算法的核心思想,先说它的作用,它的作用是实现匹配字符串时,如果遇到主串(用于对比的字符串)和模式串(拿来做对比的字符串)某个字符不相等时,将指针返回到上一个最长相等前后缀处再进行对比,这样做的好处是可以使字符不相等时退回某处,而不至于像暴力求解一样一遇到不相等就要重头对比,在主串有一定规律时,此方法显得十分方便。
下面我们再来说一说实现的方法:
定义一个数组名字叫next数组,网上大体有三种方法,第一种就是数组正常存储返回地址,第二种数组存储返回地址-1,第三种将返回地址整体向左移动。
我们说到next数组就是一个前缀表,它来存我们字符串里字符匹配失败时,应该返回的位置用第二种方法实际上是返回到最长相等前缀的位置
void getNext(int* next, const string& s){
int j = -1;
next[0] = j;
for(int i = 1; i < s.size(); i++) { // 注意i从1开始
while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了
j = next[j]; // 向前回退
}
if (s[i] == s[j + 1]) { // 找到相同的前后缀
j++;
}
next[i] = j; // 将j(前缀的长度)赋给next[i]
}
}
这一段就是c++的前缀表代码段,我们用j来表示前缀的最长长度为多少,i来向后遍历,当我们这个字符串的两个位置不相等时,判断j是否大于0,因为要涉及到j-1,只要两个位置不相等就使j一直回退,如果两位置相等,那么j向前走一步,意为最长长度加1,然后在数组每一个位置,记录当前匹配失败时,回退到那个位置。
关于kmp算法的使用我们来看具体的例题。
找出字符串中第一个匹配项的下标
28. 找出字符串中第一个匹配项的下标 - 力扣(LeetCode)
这道题就是典型可以应用kmp算法的题型
class Solution {
public:
void getNext(int* next, const string& s) {
int j = -1;
next[0] = j;
for(int i = 1; i < s.size(); i++)
{ // 注意i从1开始
while (j >= 0 && s[i] != s[j + 1])
{ // 前后缀不相同了
j = next[j]; // 向前回退
}
if (s[i] == s[j + 1])
{ // 找到相同的前后缀
j++;
}
next[i] = j; // 将j(前缀的长度)赋给next[i]
}
}
int strStr(string haystack, string needle)
{
if (needle.size() == 0)
{
return 0;
}
int next[needle.size()];
getNext(next, needle);
int j = -1; // // 因为next数组里记录的起始位置为-1
for (int i = 0; i < haystack.size(); i++)
{ // 注意i就从0开始
while(j >= 0 && haystack[i] != needle[j + 1])
{ // 不匹配
j = next[j]; // j 寻找之前匹配的位置
}
if (haystack[i] == needle[j + 1])
{ // 匹配,j和i同时向后移动
j++; // i的增加在for循环里
}
if (j == (needle.size() - 1) )
{ // 文本串s里出现了模式串t
return (i - needle.size() + 1);
}
}
return -1;
}
};
第一部分的代码就是上面所说的前缀和,后面的是用循环来遍历,也就是应用前缀和数组next,遍历查找,如果两字符串某个字符不相等,那么就使j一直回退,相等时候j++,当j++到模式串的最后一个字符了,则说明该字符串在主串中,由于字符不相同时,只有j会回退,i仍然向前走,所以当i走完全部,j还没有到到最后一个字符,说明该字符串不存在于主串内。
重复的子字符串
459. 重复的子字符串 - 力扣(LeetCode)
移动匹配
判断字符串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;
}
};
kmp算法
这道题刚看的时候,不太像使用kmp算法的题目,但是可以用kmp算法,其中是有一些技巧的:字符串的相等前后缀,错开的那一部分则为重复子串,用该重复子串重复n次,则可以取得原字符串。这个结论可以模拟一下。用next数组模拟,数组中最后一位存取的就是最长前后缀的长度,用字符串总长度,对总长度减去最长前后缀的长度,再取模,如果能整除,则该字符串是由前x个字符由n次变换得到,若不能整除则说明字符串不是由重复字串得到的。这里使用了前缀表统一减一的实现方式:
class Solution {
public:
void getNext (int* next, const string& s)
{
next[0] = -1;
int j = -1;
for(int i = 1;i < s.size(); i++){
while(j >= 0 && s[i] != s[j + 1])
{
j = next[j];
}
if(s[i] == s[j + 1])
{
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] != -1 && len % (len - (next[len - 1] + 1)) == 0)
{
return true;
}
return false;
}
};
总结:
今天我们进行了字符串的复习和双指针的回顾,还有kmp算法的应用,kmp算法还是挺有难度的,相关的思想需要多复习回顾。接下来,我们继续进行算法练习·。希望我的文章和讲解能对大家的学习提供一些帮助。
当然,本文仍有许多不足之处,欢迎各位小伙伴们随时私信交流、批评指正!我们下期见~