在信息学竞赛里,字符串处理是出现频率最高的考点之一。别被“字符串”三个字吓到,它本质上就是一串字符的集合。为了让你直观感受,我们用一个生活场景切入:
想象你在玩文字冒险游戏,屏幕上一行行显示着剧情文本。如果你想知道主角名字里有多少个字母,或者要把所有感叹号替换成问号,你不可能把整段话一次性“吞”进去再处理,而是必须一个字一个字地看过去,遇到目标就动手,遇到无关的就跳过。这种“逐字扫描”的能力,就是字符串处理的核心。
再看一个极小的暴力例子:假设有一个只包含 a 和 b 的字符串 "abba",题目要求统计 a 和 b 各出现几次。最笨的办法(也是计算机最初级的办法)就是:从第一个字符开始,如果是 a 就 a 计数 +1;如果是 b 就 b 计数 +1;然后移到下一个字符,重复这个过程,直到字符串结束。虽然笨,但逻辑清晰,不会出错。
在 C++ 中,这种“逐字扫描”通常通过循环来实现。因为字符串在内存里是连续存放的,我们可以用下标 s[i] 访问第 i 个字符,或者用范围 for 循环遍历。
| 题干信号 | 优先考虑什么 | 对应思路 |
|---|---|---|
| “统计每个字母出现次数”、“找出出现最多的字符” | 遍历 + 计数 | 开数组 cnt[256] 做桶计数 |
“把字符串里的 * 换成 -"、“凯撒密码移位” | 遍历 + 修改 | 循环里 if 判断字符类型并替换 |
| “判断是不是回文”、“正反读一样吗” | 对称比较 | 双指针从两头往中间扫 |
| “解析 ISBN 号码”、“展开字符串”、“校验格式” | 分段处理 | 状态机:识别数字段/字母段/符号段 |
| “输入一行包含空格的话” | 读入方式 | 用 getline 或 fgets,别用 cin >> |
C++ 里的字符串,本质是一个装着字符的数组,末尾额外放一个 '\0' 当“到此结束”的标记。
char s[10] = "hello";
// 内存里实际是: h e l l o \0 ? ? ? ?
// ↑ s[0] ↑ s[5] 是结束标记所以有个必须记住的结论:存 n 个字符,数组至少要开 n + 1,多出来的那一位是留给 '\0' 的。
️ 为什么很多人“看得懂但不会用”:教材上讲的是strlen/strcpy这些函数,但编程题里几乎不会让你直接调它们——题目要的是“逐字符处理”“统计”“转换”“解析”。也就是说,你真正需要的能力不是“会背函数表”,而是把字符串当成一串可以按位操作的字符来处理。这一节就讲这个。
三条基础事实(先记牢):
1. 字符就是数字(ASCII 码)。'0' 是 48,'A' 是 65,'a' 是 97。所以 s[i] - '0' 能直接把字符数字变成整数值。
2. 大小写只差 32(第 5 位):'a' - 'A' == 32。所以 s[i] ^ 32 可以切换大小写。
3. '\0' 是结束标记,值为 0。所以 for (int i = 0; s[i]; i++) 就是“一直遍历到结尾”——这个布尔判断的写法要熟练到不用想。
常用函数速查(#include <cstring>):
| 函数 | 作用 | 坑 |
|---|---|---|
strlen(s) | 求长度(不含 '\0') | O(n),别放在循环条件里 |
strcpy(d, s) | 复制 | 目标空间要够 |
strcat(d, s) | 追加 | 目标空间要够 |
strcmp(a, b) | 比较:<0 / 0 / >0 | 不是返回 true/false |
strchr(s, c) | 查字符第一次出现 | 返回指针 |
strstr(s, t) | 查子串 | 返回指针 |
输入方式对比(选错就丢数据):
| 写法 | 读到空格/换行时 |
|---|---|
scanf("%s", s) / cin >> s | 停(读到空格就结束,空格后面的内容留在缓冲区) |
fgets(s, sizeof(s), stdin) | 继续读,但会把 '\n' 一起存进去 |
cin.getline(s, n) / getline(cin, str) | 继续读,不含 '\n' ← 读整行推荐这个 |
| 知识点(对应小节) | 它解决什么 | 典型写法 | 什么时候想到它 |
|---|---|---|---|
| 逐字符扫描 | 统计 / 转换每个字符 | for(char c:s) + 桶 cnt[c-'a'] | 出现「每个字符 / 字母出现几次」 |
| 字符↔数字 | 数位和、格式输出 | c-'0'、'0'+d | 数位统计、ISBN / 多项式输出 |
| 回文 / 匹配 | 对称判断、子串比较 | 双指针 s[i] vs s[n-1-i] | 出现「回文 / 对称 / 反转」 |
| 状态机解析 | 按规则逐段翻译 | 枚举状态 state + switch | 出现「按规则展开 / 翻译 / 校验」 |
① 题目描述
给定一个只包含小写字母的字符串 s 和一个整数 k(0 <= k < 26)。将字符串中的每个字母向后移动 k 位(z 之后回到 a),输出变换后的字符串。
例如:输入 s = "abc", k = 2,输出 "cde";输入 s = "xyz", k = 2,输出 "zab"。
② 审题分析
(c - 'a' + k) % 26)来计算新字符,只能写一堆 if-else 判断,代码会极其冗长且易错。③ 解题思路
1. 定义字符数组 s 存储输入字符串。
2. 使用 for 循环,从 i = 0 遍历到字符串末尾(利用 s[i] 为 '\0' 时跳出)。
3. 在循环内部,判断 s[i] 是否为小写字母 'a' 到 'z'。
4. 如果是,计算新字符:先将 s[i] 减去 'a' 归零到 0~25,加上 k,再对 26 取模,最后加上 'a' 还原为字符。
5. 如果不是小写字母(虽然题目限定只有小写,但写个 else 更健壮),直接跳过。
④ C++ 完整参考代码
#include <iostream>
#include <cstring> // 用于 strlen
using namespace std;
int main() {
// 1. 定义字符数组,假设最大长度 100
char s[100];
int k;
// 2. 输入字符串和移位量
cin >> s >> k;
// 3. 遍历字符串,直到遇到结束符 '\0'
for (int i = 0; s[i]; i++) {
// 4. 判断当前字符是否为小写字母
if (s[i] >= 'a' && s[i] <= 'z') {
// 5. 计算新字符:归零 -> 加 k -> 取模 -> 加回 'a'
// (s[i] - 'a' + k) % 26 确保结果在 0~25 之间
s[i] = 'a' + (s[i] - 'a' + k) % 26;
}
// 6. 非小写字母直接跳过(本题无需处理)
}
// 7. 输出结果
cout << s << endl;
return 0;
}⑤ 代码解读(含真实运行输出)
这段代码的核心在于循环条件 s[i]。因为 char 类型在 C++ 中可以直接参与布尔判断,当 s[i] 是 '\0'(值为 0)时,条件为假,循环自动终止,无需手动判断 s[i] != '\0'。
运行示例:
abc 2a 变成 c,b 变成 d,c 变成 e。cdexyz 2x(23) + 2 = 25 -> y;y(24) + 2 = 26 -> 0 -> a;z(25) + 2 = 27 -> 1 -> b。zab① 题目描述
给定一个长度为 10 的字符串,表示 ISBN 号码(格式为 9 位数字 + 1 位校验码)。前 9 位数字分别乘以 10, 9, 8, ..., 2,求这 9 个乘积之和,再对 11 取模。如果余数为 10,则校验码为 X;否则校验码为余数对应的数字字符。
例如:输入 0306406152,前 9 位乘积和为 1595,1595 % 11 = 10,所以校验码是 X,输出 0306406152X。
② 审题分析
'0'~'9' 转换为整数 0~9,然后进行数学运算。'2' 参与乘法,结果会是巨大的无意义数值(因为字符本质是 ASCII 码)。③ 解题思路
1. 定义字符数组 s 存储 ISBN。
2. 初始化 sum = 0。
3. 遍历前 9 个字符(i 从 0 到 8)。
4. 在循环内,用 s[i] - '0' 将字符转为数字。
5. 乘以对应的权重 (9 - i) 并累加到 sum。
6. 计算 sum % 11。
7. 根据余数输出校验码:10 输出 X,否则输出数字字符。
④ C++ 完整参考代码
#include <iostream>
#include <cstring>
using namespace std;
int main() {
// 1. 定义字符数组,ISBN 固定 10 位
char s[11]; // 10 位数字 + 1 个 '\0'
int sum = 0;
// 2. 输入 ISBN 字符串
cin >> s;
// 3. 遍历前 9 位数字
for (int i = 0; i < 9; i++) {
// 4. 字符 '2' 减去 '0' 得到整数 2
int digit = s[i] - '0';
// 5. 权重从 10 递减到 2
sum += digit * (9 - i);
}
// 6. 计算余数
int remainder = sum % 11;
// 7. 输出结果
cout << s;
if (remainder == 10) {
cout << "X";
} else {
// 8. 余数对应的数字字符:'0' + remainder
cout << (char)('0' + remainder);
}
cout << endl;
return 0;
}⑤ 代码解读(含真实运行输出)
关键技巧是 s[i] - '0'。因为 ASCII 码中 '0' 是 48,'1' 是 49,...,'9' 是 57,它们连续排列。减去 '0' 相当于把字符映射到 0~9 的整数区间。
运行示例:
0306406152- i=0: '0'->0, 0*9=0
- i=1: '3'->3, 3*8=24
- i=2: '0'->0, 0*7=0
- i=3: '6'->6, 6*6=36
- i=4: '4'->4, 4*5=20
- i=5: '0'->0, 0*4=0
- i=6: '6'->6, 6*3=18
- i=7: '1'->1, 1*2=2
- i=8: '5'->5, 5*1=5
- sum = 0+24+0+36+20+0+18+2+5 = 105
- 105 % 11 = 6
030640615260306406152 (假设题目数据不同,比如前 9 位乘积和为 1595)0306406152X共性特征:
s[i] 访问每个字符。c - '0')或字符间的移位(c - 'a' + k)。'\0' 作为循环结束的标志,或者固定长度遍历。各自技巧:
X)。审题模板(固定思考流程):
1. 看输入:是字符串还是数字?有没有空格?长度固定吗?
2. 看要求:是统计、转换、还是校验?
3. 定策略:需要逐字符扫描吗?需要字符转数字吗?
4. 写循环:for (int i = 0; s[i]; i++) 还是固定长度?
5. 写转换:s[i] - '0' 还是 s[i] - 'a'?
错误清单只列上文没讲过的特有错误:
n 个字符要开 n + 1,忘了 '\0' 是最经典的越界原因。strlen 放进循环条件:每次循环都重算长度,时间复杂度变成 O(n²),必须先把长度存变量里。strcmp 当成布尔用:if (strcmp(a, b)) 判断的是“不相等”(因为相等返回 0)。要写 if (strcmp(a, b) == 0) 才是相等。scanf("%s") 读不到空格:要读整行请用 getline(C++)或 fgets。注意 fgets 会把 '\n' 也读进来,记得处理掉。char 是有符号的:某些平台上 char 取值范围是 -128~127,对非 ASCII 字符做下标会越界。用 unsigned char 或 int 接一下。'\0' 在中间的情况:手工改字符串时(比如删字符),别忘了补 s[newLen] = '\0',否则 strlen 会读到垃圾。+:C 风格 char 数组不能用 +(那是 std::string 才支持的)。C 风格要用 strcat。| 技法分层 | 解决什么 | 典型写法 |
|---|---|---|
| 逐字符扫描 | 统计 / 转换每个字符 | for(int i=0;s[i];i++) + 桶 cnt[c-'a'] |
| 字符↔数字 | 数位和、格式输出 | c-'0'、'0'+d |
| 回文 / 匹配 | 对称判断、子串比较 | 双指针 s[i] vs s[n-1-i] |
| 状态机解析 | 按规则逐段翻译 | 枚举状态 state + switch |
分工说明:触发清单告诉你“什么时候想到它”,速查表告诉你“具体怎么写”,总表则按粒度从细到粗归纳了所有技法。
1. 数组开小了。 存 n 个字符要开 n + 1,忘了 '\0' 是最经典的越界原因。
2. strlen 放进循环条件。
```cpp
for (int i = 0; i < strlen(s); i++) // 每次都重算,O(n²)
int len = strlen(s); // 先存下来
for (int i = 0; i < len; i++)
```
3. scanf("%s") / cin >> s 读不到空格后面的内容。 要读整行请用 getline(C++)或 fgets。注意 fgets 会把 '\n' 也读进来,记得处理掉。
4. strcmp 当成布尔用。 if (strcmp(a, b)) 判断的是"不相等"(因为相等返回 0)。要写 if (strcmp(a, b) == 0) 才是相等。
5. char 是有符号的。 某些平台上 char 取值范围是 -128~127,对非 ASCII 字符做下标会越界。用 unsigned char 或 int 接一下。
6. 忘了 '\0' 在中间的情况。 手工改字符串时(比如删字符),别忘了补 s[newLen] = '\0',否则 strlen 会读到垃圾。
7. 两个字符串相加用 +。 C 风格 char 数组不能用 +(那是 std::string 才支持的)。C 风格要用 strcat。
1. 存 "12345" 需要 char 数组至少多大?为什么?
2. for (int i = 0; i < strlen(s); i++) 的时间复杂度是多少?怎么改成 O(n)?
3. 写出把 char c 转成大写字母的两种写法(用函数 / 不用函数)。
4. 给定一个字符串,怎么判断它能不能通过重排变成一个回文串?
5. if (strcmp(a, b)) 这个判断什么时候为真?
6. scanf("%s", s) 和 getline(cin, s) 在读 "abc def" 时分别得到什么?
拿到一道字符串题,别急着敲代码,先让脑子跑一遍这个流水线:
1. 先看输入格式 —— 有没有空格?是不是整行?先确定用 cin >> 还是 getline(选错直接丢数据);
2. 看题目要“处理字符”还是“处理数字” —— 处理字符就逐位扫;处理数字先想“转不转成字符串做”;
3. 找关键词判断套路 —— “次数”→桶计数;“回文/对称”→双指针;“格式/合法”→逐段解析;
4. 想清楚字符串有多长 —— 开 char 数组要 n + 1;长度到 1e6 就别用 strlen 反复算;
5. 把边界情况列出来 —— 空串、长度为 1、开头结尾是特殊字符、有前导零。字符串题的失分几乎全在边界;
6. 提交前用“边界样例”自测,别靠眼睛看代码。
题目 A:字母求和
给定一个只包含小写字母的字符串 s,求其中所有字母的 ASCII 码之和。
输入:abc
输出:969('a' 是 97,'b' 是 98,'c' 是 99,97+98+99=294,等等,这里只是示意,实际按 ASCII 算)。
题目 B:数字替换
给定一个字符串 s 和一个整数 k。将字符串中所有数字字符替换为 (数字 + k) % 10 对应的字符,其他字符保持不变。
输入:12345,k = 5
输出:67890
学完这个考点,用历年真题检验一下
OI伴学 把「字符数组与字符串处理」在 CCF-GESP / CSP / NOIP 真题里出现过的题目汇总到一页,可按考点横向刷;每题都带答案、解题思维和常见错误。
看 字符数组与字符串处理 历年真题汇总 → 去平台刷题 →