目录
题目链接:151. 反转字符串中的单词 - 力扣(LeetCode)
题目链接:28. 找出字符串中第一个匹配项的下标 - 力扣(LeetCode)
题目链接:459. 重复的子字符串 - 力扣(LeetCode)
前言
LeetCode151.翻转字符串中的单词
KamaCoder55. 右旋字符串
LeetCode28.找出字符串中第一个匹配项的下标
LeetCode459.重复的子字符串
一、LeetCode151.翻转字符串中的单词
题目链接:151. 反转字符串中的单词 - 力扣(LeetCode)
代码:
class Solution {
public:
void reverse(string& s, int start, int end)
{
while(end > start)
{
swap(s[start++], s[end--]);
}
}
//去除字符串中多余的空格
void removeExtraSpace(string& s)
{
int slow = 0;
for(int fast = 0; fast < s.size(); fast++)
{
if(slow != 0 && s[fast] != ' ')
s[slow++] = ' ';
while(fast < s.size() && s[fast] != ' ')
{
s[slow++] = s[fast++];
}
}
s.resize(slow);
}
string reverseWords(string s) {
// 1.去除空格
removeExtraSpace(s);
// 2.整体翻转
reverse(s, 0, s.size() - 1);
// 3.单词翻转
int start = 0;
for(int i = 0; i <= s.size(); i++)
{
if(i == s.size() || s[i] == ' ')
{
reverse(s, start, i - 1);
start = i + 1;
}
}
return s;
}
};
代码思路:
①去除字符串的中多余的空格,确保字符串首尾都是字符,且每个单词之间只间隔一个空格。
②对原字符串进行反转,这样就可以让单词的整体位置进行翻转。
③通过空格和字符串长度的判断,定位单词的起始位置,对单词进行翻转
二、KamaCoder55. 右旋字符串
题目链接:55. 右旋字符串(第八期模拟笔试)
代码:
#include <iostream>
#include <string>
using namespace std;
void Reverse(string& s, int start, int end)
{
while(start < end)
{
swap(s[start++], s[end--]);
}
}
int main()
{
int n;
string s;
cin >> n >> s;
Reverse(s, 0, s.size() - n - 1);
Reverse(s, s.size() - n, s.size() - 1);
Reverse(s, 0, s.size() - 1);
cout << s << endl;
return 0;
}
代码思路:
从下图可以看出,要想从源字符串到目的字符串,可以通过整体翻转加局部翻转实现。
三、LeetCode28.找出字符串中第一个匹配项的下标
题目链接:28. 找出字符串中第一个匹配项的下标 - 力扣(LeetCode)
KMP算法代码:
class Solution {
public:
void getNext(vector<int>& next, 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;
}
}
int strStr(string haystack, string needle) {
vector<int> next(needle.size());
getNext(next, needle);
int j = 0;
for(int i = 0; i < haystack.size(); i++)
{
// while是为了找到haystack[i]与needle字符串中最长前后缀字符串的位置
// 1、如果haystack[i] != needle[j]则表示当前needle第j之前的字符串虽然匹配,
// 但是needle[j]这个位置并不匹配
// 2、如果一直到j = 0,则表示不存在前后缀匹配的字符串,接下来needle从头开始
// 与haystack比较即可
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;
}
};
KMP算法思路:
next数组含义:next[i]表示从第一个字符到第i个字符这个范围的最长前后缀匹配字符串的长度。这个最长前后缀字符串不能是整个字符串,必须是长度小于原字符串的子串!如下图:
①第一个问题:什么是最长前后缀匹配字符串长度?
最长前后缀匹配字符串就是一个字符串s的头部一部分子串和尾部一部分子串完全相同且子串长度在满足前后缀匹配的条件下是最长的。
例如:
字符串"ab",由于要求子串不能跟字符串等长,所以最长的子串长度为1,即前缀为"a",后缀为"b",前后缀子串不匹配,所以最长前后缀匹配字符串长度为0。
字符串"aba",前缀子串"a",后缀子串"a",最长前后缀匹配字符串长度为1
字符串"abab",前缀子串"ab",后缀子串"ab",最长前后缀匹配字符串长度为2
了解完什么是最长前后缀字符串长度之后,我们就可以求字符串"aabaaabaac"的next数组了。求解过程如下:
通过上图过程,可以得到对应的next数组如下:
②第二个问题:next数组的作用是什么?
四、LeetCode459.重复的子字符串
题目链接:459. 重复的子字符串 - 力扣(LeetCode)
暴力解法代码:
class Solution {
public:
bool repeatedSubstringPattern(string s) {
for(int i = 0; i < s.size() / 2; i++)
{
for(int j = i + 1, k = 0; j < s.size();)
{
if(k == i + 1)
k = 0;
if(s[j] == s[k])
{
j++;
k++;
if(k == i + 1 && j == s.size())
return true;
}
else
{
break;
}
}
}
return false;
}
};
暴力解法代码思想:
第一层for循环是确认子串的终止位置,因为一个字符串既然能被一个子串重复表示出来,那么这个重复子串一定在开头位置处就能找到。第二层for循环用来检索是否后续的字符串是否可用开头的子串完全表示出来,如果能完全表示出来,那么就会一直满足"s[j] == s[k]"的条件,直到 j 遍历到字符串的结尾并且 k 遍历也刚刚好又再次遍历完开头的子串。
移动匹配代码:
class Solution {
public:
bool repeatedSubstringPattern(string s) {
string ss = s + s;
ss.erase(ss.size() - 1, 1);
ss.erase(0, 1);
if(ss.find(s) != string::npos)
return true;
return false;
}
};
移动匹配代码思路:
移动匹配代码的思路是基于存在重复子串的前提下进行的。例如s = "abab",由两个s组成的ss,即ss = "abababab"中一定在中间存在子串s,即"abab",也就是说ss字符串的中间必定存在子串"abab"。
可以通过如下方式证明:
已有条件:s有4个字符,分别为s[0], s[1], s[2], s[3]。其中存在重复子串。
证明:ss = s + s,其中ss中间位置一定存在子串s。
ss可以表示为ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7],其中ss[0] = ss[4], ss[1] = ss[5], ss[2] = ss[6], ss[3] = ss[7](因为ss = s + s,所以前后对应位置字符相同)。由于存在重复子串,所以字符串s的前后缀一定存在匹配关系,这个关系可能是1个或者2个字符的匹配关系(因为字符串s长度为4,最长重复子串的长度只能是2,最小是1),假设重复子串长度是2,则可得s[0] = ss[2],s[1] = ss[3]。同理可得s[2] = ss[4], s[3] = ss[5]。所以必然ss中必然存在子串s。
以上可证明的是充分条件。
必要条件证明只要根据ss中间存在子串s进行反推,即可得到s[0] = ss[2],s[1] = ss[3],可得s[0] = s[2],s[1] = s[3],即可求得s中存在重复子串。
结论:满足以上充分必要条件,我们就可以得到如果字符串s中有重复子串,那么必然存在ss = s + s,在ss中间存在子串s。
根据结论,我们可以写出代码,只要将ss = s + s,然后去掉ss的首字符和尾字符,再从ss中找是否存在子串s,如果存在说明有重复子串,如果不存在子串s,说明没有重复子串。
KMP解法代码:
class Solution {
public:
void getNext(vector<int>& next, 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) {
vector<int> next(s.size());
getNext(next, s);
if(next[s.size() - 1] != 0 && s.size() % (s.size() - next[s.size() - 1]) == 0)
return true;
return false;
}
};
KMP代码思路:
KMP的代码思想跟移动匹配的思想类似,我们假设字符串s的长度为6,即s[0], s[1], s[2], s[3], s[4], s[5]。通过求出next数组,我们假设几种情况,
①第一种情况:next[5] = 4;即可得前后缀有长度为4的最长匹配字符串。也就可以得到s[0] = s[2], s[1] = [3], s[2] = s[4], s[3] = [5]。也就可以得到s[0] = s[2] = s[4], s[1] = s[3] = s[5]。也就可以得出存在重复子串(s[0], s[1]),子串为最长后缀之前的的字符串。
②第二种情况:next[5] = 3; 即s[0] = s[3], s[1] = s[4], s[2] = s[5],这直接就可以得到(s[0], s[1], s[2])就是重复子串了,子串为最长后缀之前的的字符串。
③第三种情况,如果next[5] = 2,即s[0] = s[4], s[1] = s[5],并且还可以得到s[0] != s[3], s[0] != [2], s[0] != s[1]等多个条件(从next数组求解过程可知),所以一定不存在重复子串。
由上述情况可知,通过next数组如果找得到重复子串,则重复子串长度必然为最长后缀之前的字符串,即重复子串的长度为(s.size() - next[s.size() - 1])。那么当字符串s的长度能被重复子串长度整除时,则字符串s必然包含重复子串。