右旋字符串
题干
字符串的右旋转操作是把字符串尾部的若干个字符转移到字符串的前面。给定一个字符串 s 和一个正整数 k,请编写一个函数,将字符串中的后面 k 个字符移到字符串的前面,实现字符串的右旋转操作。
例如,对于输入字符串 “abcdefg” 和整数 2,函数应该将其转换为 “fgabcde”。
题解思路
整体思路就是先整体反转,再分段反转,前面n-k个字符串为一段,后面k个为另一段,
#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
string s;
int n;
cin >> n;
cin >> s;
reverse(s.begin(),s.end());
reverse(s.begin(),s.begin()+n);
reverse(s.begin()+n,s.end());
cout<<s<<endl;
}
注意点:
- 注意两个头文件
- 注意
cin >>s;
的用法 - 注意
cout << s <<endl;
的用法
28. 实现 strStr()
题干
给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。
示例 1: 输入: haystack = “hello”, needle = “ll” 输出: 2
示例 2: 输入: haystack = “aaaaa”, needle = “bba” 输出: -1
题解思路
KMP算法
- 名字由来:
由这三位学者发明的:Knuth,Morris和Pratt,所以取了三位学者名字的首字母。所以叫做KMP - KMP算法有什么用
KMP主要应用在字符串匹配上。KMP的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。 - KMP算法的原理
给出一个文本串aabaabaaf
,和一个模式串aabaaf
,现在要求在文本串中是否出现过模式串?
前缀表
首先要区分清楚,什么是前缀什么是后缀?以模式串举例,
前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。 前缀表的任务是在当前位置匹配失败时,找到之前已经匹配上的位置,再重新匹配,此也意味着在某个字符失配时,前缀表会告诉你下一步匹配中,模式串应该跳到哪个位置。
KMP算法步骤
这里对模式串指针的移动目的就是为了使模式串再次与文本串对齐
代码实现
定义数组next来存储前缀表的数据,
定义一个函数getNext来构建next数组,函数参数为指向next数组的指针,和一个字符串。
构造next数组其实就是计算模式串s,前缀表的过程。 主要有如下三步:
- 初始化
- 处理前后缀不相同的情况
- 处理前后缀相同的情况
这种方法本质上是采用了一种递推的方式来求解next数组,它的巧妙之处就是在于会不断利用已经掌握的信息来避免重复的运算,
两侧指针同时移动,如果下一个字符两侧都相同的话,那么next数组对应的元素直接在前一个的基础上加1即可,
但是如果下一个字符不同的话,既然ABA无法与下一个字符构成更长的前后缀,那我们就看看其中存不存在更短的,比如AB,关键就是这一步的求解方法,根据之前的计算我们掌握了一个重要信息,由于ABA这两个子串完全一样,其最大前后缀长度也一样,我们直接在左边的ABA子串中寻找最大前后缀就好了,而左边的ABA子串最大前后缀长度我们之前已经计算出来了,就是next[2]即1,
接下来重复最开始的步骤,检查下一个字符是否相等,如果相同那么就可以构成一个更长的前后缀,
求next数组的过程与利用next数组进行字符匹配的过程(即KMP过程)中指针回退的逻辑是一样的,在求next数组的过程中,需要利用前面字符串的最大相等前后缀长度确定回退的位置,进而继续进行前后字符匹配,计算新的最大相等前后缀;而KMP过程中也需要利用前面字符串的最大相等前后缀长度确定指针回退的位置,进而使模式串和文本串对齐,继续进行字符查找
void getNext(int* next, const string& s) {
int j = 0;
next[0] = 0;
for(int i = 1; i < s.size(); i++) {
while (j > 0 && s[i] != s[j]) { // j要保证大于0,因为下面有取j-1作为数组下标的操作
j = next[j - 1]; // 注意这里,是要找前一位的对应的回退位置了
}
if (s[i] == s[j]) {
j++;
}
next[i] = j;
}
}
KMP实现代码如下:
class Solution {
public:
void getNext(int* next, const string& s) {
int j = 0;
next[0] = 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) {
if (needle.size() == 0) {
return 0;
}
vector<int> next(needle.size());
getNext(&next[0], needle); //这里必须用&next[0]作为参数,不能将next作为参数传入
int j = 0;
for (int i = 0; i < haystack.size(); i++) { //注意这里的i起始是从0开始,
while(j > 0 && haystack[i] != needle[j]) { //注意这里的区别 haystack[i] != needle[j]
j = next[j - 1];
}
if (haystack[i] == needle[j]) { //注意这里的区别 haystack[i] != needle[j]
j++;
}
if (j == needle.size() ) {
return (i - needle.size() + 1);
}
}
return -1;
}
};
注意点
- 可以直接定义一个数组来存储next的数据
int next[needle.size()];
GetNext(next,needle);
使用const string&
来传递字符串,避免不必要的复制,提高效率。
引用操作
引用(&)是C++中的一种轻量级指针,它为已存在的变量创建一个别名。与指针不同,引用必须在声明时初始化,并且之后不能改变指向的对象。
常量引用(const T&)是指向常量的引用,表示引用的对象不能通过此引用修改。这为代码增加了安全性和可读性,防止意外修改数据。
459.重复的子字符串
题干
给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成。给定的字符串只含有小写英文字母,并且长度不超过10000。
示例 1:
输入: “abab”
输出: True
解释: 可由子字符串 “ab” 重复两次构成。
解题思路
两种方法
1. 移动匹配
原字符串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;
}
};
注意点
- 移除字符串的最后一个字符为什么要用New.end() - 1?
New.end()是指向字符串末尾的迭代器(即在字符串最后一个字符的后一个位置)。New.end() - 1则是指向字符串最后一个字符的迭代器。使用New.erase(New.end() - 1)即可移除最后一个字符。因为标准库的erase方法需要一个有效的迭代器,而不是超出范围的迭代器。 - 如果没有查找到目标字符串,为什么不是返回New.end()?
std::string::find
函数返回的是size_t类型的值,表示找到的子串的起始位置。如果找不到子串,则返回std::string::npos
。这与New.end()(这是一个迭代器类型)不同。
2. KMP算法
在由重复子串组成的字符串中,最长相等前后缀不包含的子串就是最小重复子串,这里拿字符串s:abababab 来举例,ab就是最小重复单位,如图所示:
步骤一:因为这是相等的前缀和后缀,t[0] 与 k[0]相同, t[1] 与 k[1]相同,所以 s[0] 一定和 s[2]相同,s[1] 一定和 s[3]相同,即:,s[0]s[1]与s[2]s[3]相同 。
步骤二: 因为在同一个字符串位置,所以 t[2] 与 k[0]相同,t[3] 与 k[1]相同。
步骤三: 因为 这是相等的前缀和后缀,t[2] 与 k[2]相同 ,t[3]与k[3] 相同,所以,s[2]一定和s[4]相同,s[3]一定和s[5]相同,即:s[2]s[3] 与 s[4]s[5]相同。
步骤四:循环往复。
所以字符串s,s[0]s[1]与s[2]s[3]相同, s[2]s[3] 与 s[4]s[5]相同,s[4]s[5] 与 s[6]s[7] 相同。
正是因为最长相等前后缀的规则,当一个字符串由重复子串组成的,最长相等前后缀不包含的子串就是最小重复子串。
这个字符串的最大相等前后缀的长度就是next数组的最后一个位置的数据,即next[size-1]
,如果原字符串的长度减去next[size-1]
之后可以被原字符串的长度整除,那就说明原字符串是由重复子串构成的。
class Solution {
public:
void getNext (int* next, const 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) {
if (s.size() == 0) {
return false;
}
int next[s.size()];
getNext(next, s);
int len = s.size();
if (next[len - 1] != 0 && len % (len - (next[len - 1] )) == 0) {
return true;
}
return false;
}
};