字符串理论基础总结:
什么是字符串,字符串是若干字符组成的有限序列,也可以理解为是一个字符数组,但是很多语言对字符串做了特殊的规定,我们以C/C++中的字符串举例。
在C语言中,把一个字符串存入一个数组时,也把结束符'\0'存入数组,并以此作为该字符是否结束的标志。
char a[5] = "asd";
for (int i = 0; a[i] != '\0'; i++) {
}
在C++中,提供一个 string 类,string 类会提供size接口,可以用来判断 string 类字符串是否结束,就不用'\0'来判断是否结束。
string a = "asd";
for (int i = 0; i < a.size(); i++) {
}
那么 vector<char> 和 string 又有什么区别呢?
其实在基本操作上没有区别,但是 string 提供更多的字符串处理的相关接口,例如 string 重载了 '+',而 vector 却没有。
所以想处理字符串,我们还是会定义一个 string 类型。
字符串的经典题目:
/*
思路分析:双指针法
*/
class Solution {
public:
void reverseString(vector<char>& s) {
int left = 0;
int right = s.size() - 1;
while(left <= right) {
swap(s[left],s[right]);
left++;
right--;
}
}
};
/*
思路分析:当需要固定规律一段一段去处理字符串的时候,要想想在在for循环的表达式上做做文章
一些同学可能为了处理逻辑:每隔2k个字符的前k的字符,写了一堆逻辑代码或者再搞一个计数器,来统计2k,再统计前k个字符。
其实在遍历字符串的过程中,只要让 i += (2 * k),i 每次移动 2 * k 就可以了,然后判断是否需要有反转的区间。
*/
class Solution {
public:
string reverseStr(string s, int k) {
for (int i = 0; i < s.size(); i += (2 * k)) {
// 1. 每隔 2k 个字符的前 k 个字符进行反转
// 2. 剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符
if (i + k <= s.size()) {
reverse(s.begin() + i, s.begin() + i + k );
} else {
// 3. 剩余字符少于 k 个,则将剩余字符全部反转。
reverse(s.begin() + i, s.end());
}
}
return s;
}
};
/*
思路分析:解决方案有很多,但要想做到极致,我们就不使用额外的辅助空间了。
1.首先将数组扩充到每个数字字符替换成 'number' 之后的大小
2.双指针法,从后向前替换字符
*/
int main() {
std::string str;
while(std::cin >> str) {
int oldIndex = str.size() - 1;
int numberCount = 0;
for(char c : str) {
if(c >= '0' && c <= '9') {
numberCount++;
}
}
str.resize(str.size() + numberCount * 5);
int newIndex = str.size() - 1;
while(oldIndex >= 0) {
if(str[oldIndex] >= '0' && str[oldIndex] <= '9') {
str[newIndex--] = 'r';
str[newIndex--] = 'e';
str[newIndex--] = 'b';
str[newIndex--] = 'm';
str[newIndex--] = 'u';
str[newIndex--] = 'n';
} else {
str[newIndex--] = str[oldIndex];
}
oldIndex--;
}
std::cout << str << std::endl;
}
return 0;
}
/*
思路分析:1.整体反转字符串
2.逐个反转每个单词
3.移除多余空格
*/
class Solution {
public:
string reverseWords(string s) {
// 1.整体反转字符串
reverse(s.begin(), s.end());
// 2.逐个反转每个单词
int start = 0, end = 0;
int n = s.size();
while(start < n) {
// 找到单词的起点
while(start < n && s[start] == ' ') start++;
// 遍历结束
if(start >= n) break;
end = start;
// 找到单词的终点
while(end < n && s[end] != ' ') end++;
// 反转单词
reverse(s.begin() + start, s.begin() + end);
start = end;
}
// 3.移除多余空格
int fastIndex = 0, lowIndex = 0;
for(; fastIndex < n; ++fastIndex) {
if(s[fastIndex] != ' ') {
// 单词之间添加空格
if(lowIndex != 0) s[lowIndex++] = ' ';
while(fastIndex < n && s[fastIndex] != ' ') {
s[lowIndex++] = s[fastIndex++];
}
}
}
// 调整字符串大小,移除末尾多余空格
s.resize(lowIndex);
return s;
}
};
/*
思路分析:将字符串分为两个部分,先整体倒序,再局部倒序,负负得正。
*/
#include<iostream>
#include<algorithm>
using namespace std;
int main() {
int n;
string s;
cin >> n;
cin >> s;
int len = s.size(); //获取长度
reverse(s.begin(), s.end()); // 整体反转
reverse(s.begin(), s.begin() + n); // 先反转前一段,长度n
reverse(s.begin() + n, s.end()); // 再反转后一段
cout << s << endl;
}
// 补充评论区答案中的巧妙思路:回文串思想 + string 内置函数 substr()
#include <iostream>
using namespace std;
int main()
{
int n;
string s;
cin >> n >> s;
int len = s.size();
cout << (s + s).substr(len - n, len) << '\n';
return 0;
}
/*
思路分析:1.暴力解法
2.KMP算法:当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配。
*/
// 暴力解法 -- 时间复杂度O(m * n)
class Solution {
public:
int strStr(string haystack, string needle) {
int lenHay = haystack.size();
int lenNeed = needle.size();
// 处理特殊情况
if (needle.empty()) return 0;
if (lenNeed > lenHay) return -1;
for (int posHay = 0; posHay <= lenHay - lenNeed; ++posHay) {
int posNeed = 0;
while (posNeed < lenNeed && haystack[posHay + posNeed] == needle[posNeed]) {
++posNeed;
}
if (posNeed == lenNeed) {
return posHay;
}
}
return -1;
}
};
// KMP算法 -- 时间复杂度O(m + n)
class Solution {
public:
vector<int> getNext(const string& str) {
vector<int> next(str.size());
int j = 0;
next[0] = 0;
for (int i = 1; i < str.size(); ++i) { // i 从 1 开始
while (j > 0 && str[i] != str[j]) {
j = next[j - 1];
}
if (str[i] == str[j]) {
++j;
}
next[i] = j;
}
return next;
}
int strStr(string haystack, string needle) {
int lenHay = haystack.size();
int lenNeed = needle.size();
// 处理特殊情况
if (needle.empty()) return 0;
if (lenNeed > lenHay) return -1;
vector<int> next = getNext(needle);
for (int i = 0, j = 0; i < haystack.size(); ++i) {
while (j > 0 && haystack[i] != needle[j]) {
j = next[j - 1];
}
if (haystack[i] == needle[j]) {
++j;
}
if (j == lenNeed) return i - j + 1;
}
return -1;
}
};
/*
思路分析:1.暴力解法:一个for循环获取 子串的终止位置, 然后判断子串是否能重复构成字符串,又嵌套一个for循环,所以是O(n^2)的时间复杂度。
2.移动匹配:用 s + s,这样组成的字符串中,后面的子串做前串,前面的子串做后串,就一定还能组成一个s。
3.KMP算法:与实现strStr()不同的是,这道题的核心在于找到最小重复串,数组长度减去最长相同前后缀的长度相当于是第一个周期的长度,也就是一个周期的长度,如果这个周期可以被整除,就说明整个数组就是这个周期的循环。
*/
class Solution {
public:
bool repeatedSubstringPattern(string s) {
int len = s.size();
vector<int> next(s.size());
next[0] = 0;
for(int i = 1, k = 0; i < len; ++i) {
while(k > 0 && s[i] != s[k]) {
k = next[k - 1];
}
if(s[i] == s[k]) {
k++;
}
next[i] = k;
}
int longest_prefix = next[len - 1];
if(longest_prefix > 0 && len % (len - longest_prefix) == 0)
return true;
return false;
}
};
总结
字符串类类型的题目,往往想法比较简单,但是实现起来并不容易,复杂的字符串题目非常考验对代码的掌控能力。
双指针法是字符串处理的常客。
KMP算法是字符串查找最重要的算法。