字符串 - 代码随想录

字符串

字符串刷题记录,基于代码随想录整理。

  • 344 反转字符串(双指针)
  • 541 反转字符串 II(分段处理)
  • 模拟 替换数字(遍历构造)
  • 151 反转字符串中的单词(去空格 + 整体反转 + 单词反转)
  • 右旋 右旋字符串(整体反转)
  • 28 找出字符串中第一个匹配项的下标(KMP)
  • 459 重复的子字符串(KMP)
  • 双指针:344、151
  • 模拟分段处理:541、替换数字、右旋字符串
  • KMP:28、459

344. 反转字符串

问题描述

编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。

不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。

示例 1:

1
2
输入:s = ["h","e","l","l","o"]
输出:["o","l","l","e","h"]

示例 2:

1
2
输入:s = ["H","a","n","n","a","h"]
输出:["h","a","n","n","a","H"]

提示:

  • 1 <= s.length <= 10^5
  • s[i] 都是 ASCII 码表中的可打印字符

思路

双指针:一个指向字符串头部,一个指向字符串尾部,同时向中间移动并交换字符。

代码

1
2
3
4
5
6
7
8
class Solution {
public:
void reverseString(vector<char>& s) {
for (int left = 0, right = s.size() - 1; left < right; left++, right--) {
swap(s[left], s[right]);
}
}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

541. 反转字符串 II

问题描述

给定一个字符串 s 和一个整数 k,从字符串开头算起,每计数至 2k 个字符,就反转这 2k 字符中的前 k 个字符,再重新计数。

  • 如果剩余字符少于 k 个,则将剩余字符全部反转。
  • 如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样。

示例 1:

1
2
输入:s = "abcdefg", k = 2
输出:"bacdfeg"

示例 2:

1
2
输入:s = "abcd", k = 2
输出:"bacd"

提示:

  • 1 <= s.length <= 10^4
  • s 仅由小写英文组成
  • 1 <= k <= 10^4

思路

按题意每次处理长度为 2k 的一段,因此遍历时直接让下标每次跳 2 * k 即可。

  • 若当前这一段剩余字符数不少于 k,就反转前 k 个字符。
  • 若剩余字符数少于 k,就把剩余部分全部反转。

当需要固定规律一段一段处理字符串时,可以优先考虑在 for 循环的步长上做文章。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
string reverseStr(string s, int k) {
for (int i = 0; i < s.size(); i += 2 * k) {
if (i + k <= s.size()) {
reverse(s.begin() + i, s.begin() + i + k);
} else {
reverse(s.begin() + i, s.end());
}
}
return s;
}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

替换数字(第八期模拟笔试)

问题描述

给定一个字符串 s,它包含小写字母和数字字符,请编写一个函数,将字符串中的字母字符保持不变,而将每个数字字符替换为 number

例如,对于输入字符串 "a1b2c3",函数应该将其转换为 "anumberbnumbercnumber"

输入描述

输入一个字符串 ss 仅包含小写字母和数字字符。

输出描述

打印一个新的字符串,其中每个数字字符都被替换为了 number

输入示例

1
a1b2c3

输出示例

1
anumberbnumbercnumber

提示信息

  • 1 <= s.length < 10000

思路

遍历字符串,判断当前字符是否落在 '0' ~ '9' 范围内:

  • 如果是数字,就向结果串追加 "number"
  • 否则直接追加原字符

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <bits/stdc++.h>
using namespace std;

int main() {
string s;
cin >> s;

string res;
for (char c : s) {
if (c >= '0' && c <= '9') {
res += "number";
} else {
res += c;
}
}

cout << res << endl;
return 0;
}
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

151. 反转字符串中的单词

问题描述

给你一个字符串 s,请你反转字符串中 单词 的顺序。

单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。

返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。

注意: 输入字符串 s 中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。

示例 1:

1
2
输入:s = "the sky is blue"
输出:"blue is sky the"

示例 2:

1
2
3
输入:s = "  hello world  "
输出:"world hello"
解释:反转后的字符串中不能存在前导空格和尾随空格。

示例 3:

1
2
3
输入:s = "a good   example"
输出:"example good a"
解释:如果两个单词间有多余的空格,反转后的字符串需要将单词间的空格减少到仅有一个。

提示:

  • 1 <= s.length <= 10^4
  • s 包含英文大小写字母、数字和空格 ' '
  • s至少存在一个 单词

思路

这道题的核心是三步:

  1. 移除多余空格
  2. 反转整个字符串
  3. 再逐个反转每个单词

这样就能在保留单词内容的同时,把单词顺序整体翻转过来。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Solution {
public:
void removeExtraSpaces(string& s) {
int slow = 0;
for (int i = 0; i < s.size(); i++) {
if (s[i] != ' ') {
if (slow != 0) {
s[slow++] = ' ';
}
while (i < s.size() && s[i] != ' ') {
s[slow++] = s[i++];
}
}
}
s.resize(slow);
}

string reverseWords(string s) {
removeExtraSpaces(s);
reverse(s.begin(), s.end());

int start = 0;
for (int i = 0; i <= s.size(); i++) {
if (i == s.size() || s[i] == ' ') {
reverse(s.begin() + start, s.begin() + i);
start = i + 1;
}
}
return s;
}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

右旋字符串(第八期模拟笔试)

问题描述

字符串的右旋转操作是把字符串尾部的若干个字符转移到字符串的前面。给定一个字符串 s 和一个正整数 k,请编写一个函数,将字符串中的后面 k 个字符移到字符串的前面,实现字符串的右旋转操作。

例如,对于输入字符串 "abcdefg" 和整数 2,函数应该将其转换为 "fgabcde"

输入描述

输入共包含两行,第一行为一个正整数 k,代表右旋转的位数。第二行为字符串 s,代表需要旋转的字符串。

输出描述

输出共一行,为进行了右旋转操作后的字符串。

输入示例

1
2
2
abcdefg

输出示例

1
fgabcde

提示信息

  • 1 <= k < 10000
  • 1 <= s.length < 10000

思路

这题有两种常见写法:

1. 复制字符串

把原串复制一份,形成 s + s,然后从 s.size() - k 位置开始连续取 s.size() 个字符。

2. 整体反转

右旋 k 位可以拆成三次反转:

  1. 整体反转
  2. 反转前 k 个字符
  3. 反转后面的剩余字符

要注意先做 k %= s.size(),避免 k 大于字符串长度时越界。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <bits/stdc++.h>
using namespace std;

int main() {
int k;
string s;
cin >> k >> s;

k %= s.size();
string tmp = s + s;
int start = s.size() - k;

for (int i = 0; i < s.size(); i++) {
s[i] = tmp[start + i];
}

cout << s << endl;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <bits/stdc++.h>
using namespace std;

int main() {
int k;
string s;
cin >> k >> s;

k %= s.size();
reverse(s.begin(), s.end());
reverse(s.begin(), s.begin() + k);
reverse(s.begin() + k, s.end());

cout << s << endl;
return 0;
}
  • 时间复杂度:O(n)
  • 空间复杂度:复制字符串写法为 O(n),整体反转写法为 O(1)

28. 找出字符串中第一个匹配项的下标

问题描述

给你两个字符串 haystackneedle,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1

示例 1:

1
2
3
4
输入:haystack = "sadbutsad", needle = "sad"
输出:0
解释:"sad" 在下标 0 和 6 处匹配。
第一个匹配项的下标是 0,所以返回 0。

示例 2:

1
2
3
输入:haystack = "leetcode", needle = "leeto"
输出:-1
解释:"leeto" 没有在 "leetcode" 中出现,所以返回 -1。

提示:

  • 1 <= haystack.length, needle.length <= 10^4
  • haystackneedle 仅由小写英文字符组成

思路

KMP 的核心思想:当出现字符串不匹配时,可以利用已经匹配过的信息,避免主串指针回退,从而提高匹配效率。

这里的关键就是 next 数组。

  • next[i] 表示以下标 i 结尾的子串中,最长相等前后缀的长度。
  • 当匹配失败时,模式串指针 j 可以直接跳到 next[j - 1],继续比较。
什么是前缀和后缀?
  • 前缀:不包含最后一个字符的、从第一个字符开始的连续子串
  • 后缀:不包含第一个字符的、以最后一个字符结尾的连续子串

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Solution {
public:
void getNext(vector<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) {
vector<int> next(needle.size(), 0);
getNext(next, needle);

int j = 0;
for (int i = 0; i < haystack.size(); i++) {
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;
}
};
  • 时间复杂度:O(n + m)
  • 空间复杂度:O(m)

459. 重复的子字符串

问题描述

给定一个非空的字符串 s,检查是否可以通过由它的一个子串重复多次构成。

示例 1:

1
2
3
输入:s = "abab"
输出:true
解释:可由子串 "ab" 重复两次构成。

示例 2:

1
2
输入:s = "aba"
输出:false

示例 3:

1
2
3
输入:s = "abcabcabcabc"
输出:true
解释:可由子串 "abc" 重复四次构成。

思路

这题也可以用 KMP。

如果一个字符串 s 可以由某个更短的子串重复多次组成,那么 s 一定存在非零的最长相等前后缀。

设字符串长度为 nnext[n - 1] 表示整个字符串的最长相等前后缀长度:

  • next[n - 1] == 0,说明不存在可复用的重复结构。
  • next[n - 1] != 0,并且 n % (n - next[n - 1]) == 0,说明字符串可以由长度为 n - next[n - 1] 的子串重复构成。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution {
public:
void getNext(vector<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;
}
}

bool repeatedSubstringPattern(string s) {
vector<int> next(s.size(), 0);
getNext(next, s);

int n = s.size();
int len = next[n - 1];
return len != 0 && n % (n - len) == 0;
}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

总结

双指针: 常用于原地反转、去空格等操作。
分段处理: 像 541 这类按固定长度处理字符串的问题,重点在循环步长。
整体反转: 右旋字符串、单词翻转都可以转化为多次反转。
KMP: 本质是利用已匹配信息加速失配后的跳转,核心在 next 数组定义与回退过程。
重复子串判断: 关键是把问题转化为最长相等前后缀。