
最近在整理字符串相关的练习题时手头遇到了这么两道题第92题“字符串统计”第93题“隐藏口令”。第一眼看过去说实话有点懵。字符串统计好歹知道是统计字符个数但具体统计哪些字符、按什么口径统计题目描述很含糊隐藏口令就更不用说了一个口令藏在字符串里怎么藏、怎么找、规则在哪里全是未知数。我刷题这些年最怕的不是难题而是这种“题目没说明白”的题。但后来沉下心来拆解后发现这种“没思路”的感觉根源往往不在题目本身而在自己脑子里没有一套字符串题的分析框架。字符串类的题目看起来千变万化什么字符串分割、字符串替换、字符串逆序、字符串转数字换个马甲又是一道新题但拆到内核无非就是几件事遍历、查找、截取、拼接、比较、转换。第92题和第93题正好是两种最典型的代表一个考你怎么“数”一个考你怎么“找”。这篇文章我把两题完整的思考过程、代码实现、以及实测中踩过的坑都写出来希望能帮到同样卡在字符串题上的朋友。1. 两道题都没思路先别急着刷答案把题目条件拆开看遇到算法题没有思路我的习惯是先把题目里的“已知条件”和“输出要求”单独拎出来。很多没思路不是因为你不会写代码而是大脑里根本没有形成一个清晰的输入输出模型。1.1 字符串统计到底要统计什么“字符串统计”这个题目不同题库里的版本差异很大。最常见的是谭浩强那类C语言教材里的经典题输入一行字符分别统计英文字母、数字、空格和其他字符的个数。还有一种考法是统计字符串中每个字符出现的次数也就是频率统计。再复杂一点是统计单词数量或者统计某个子串在主串中出现的次数。我一开始没思路是因为我把几种可能全混在一起想了。又想要字母数字空格其他又想要每个字符的频率又想要单词数越想越乱。后来我把题目的关键词拆开“字符串统计”锁定的是“统计”动作那核心就是计数。至于统计什么口径取决于题目给的输入格式。所以第一步不是写代码而是确认题目要求。如果是教材题大概率是四类统计如果是OJ在线评测题有时候会明确说“统计各字符出现次数”甚至会把输入输出样例给出来。不看样例直接开写是新手最常犯的错误。1.2 隐藏口令到底是哪类题“隐藏口令”这个题名更抽象。口令藏在字符串里最常见的有三种套路按字符类型筛比如把字符串里所有大写字母或数字提取出来拼成口令。按固定位置取每隔几个字符取一个或者去每行字符串的固定位置字符。按加密规则解口令不是明文而是经过移位、替换等简单加密后的文本需要逆运算恢复。我一开始看到“隐藏口令”四个字脑海里没有任何一条具体的规则所以整个人的状态就是“这题没法做”。后来我意识到这类题的本质其实是字符串筛选或变换你要做的就是找到题目描述中的那一条“隐藏规则”然后用代码表达出来。也就是说字符串统计的核心问题是“数什么”隐藏口令的核心问题是“怎么筛/怎么变”。一旦把这两个问题想明白代码反而不难。2. 字符串统计先把统计口径定下来再去写循环这一题真正动手之后我发现关键点不在循环体本身而在于循环体里怎么写判断条件、计数器放在哪里以及处理字符串末尾的换行符。2.1 经典四类统计字母、数字、空格、其他如果题目要求的是“输入一行字符统计英文字母、数字、空格和其他字符的个数”那最直接的解法是用一个循环遍历字符串用isalpha、isdigit这类库函数判断类型或者手动用 ASCII 码范围判断。我平时更推荐用ctype.h里的库函数因为代码可读性好而且不会因为字符编码的问题误判。#include stdio.h #include ctype.h int main() { char s[200]; int letters 0, digits 0, spaces 0, others 0; printf(请输入一行字符串); fgets(s, sizeof(s), stdin); for (int i 0; s[i] ! \0 s[i] ! \n; i) { if (isalpha(s[i])) { letters; } else if (isdigit(s[i])) { digits; } else if (s[i] ) { spaces; } else { others; } } printf(字母%d\n, letters); printf(数字%d\n, digits); printf(空格%d\n, spaces); printf(其他%d\n, others); return 0; }这里有一个非常重要的细节为什么循环条件里要判断s[i] ! \n因为fgets会把用户输入末尾的回车也读进缓冲区如果不去掉它回车符就会被统计到“其他”里面导致结果多一个计数。这是字符串统计题最经典的坑之一。如果你用的是scanf(%s, s)那确实不会读入空格和回车但它也会在遇到空格时停止后面输入的内容全部丢失。所以遇到“输入一行包含空格的字符串”时首选fgets而不是scanf。2.2 进阶统计每个字符出现的次数如果题目升级成“统计每个字符出现的次数”那就不能用四个变量了。因为你不知道字符串里会出现多少种字符开变量根本开不过来。这时候要用一个长度为 256 的整型数组来充当计数器数组下标就是字符的 ASCII 码值。原理很简单字符在内存里本质上就是一个整数比如大写字母 A 是 65小写字母 a 是 97数字字符 0 是 48。把字符转换成整数后用它当数组下标对应的数组元素就一直自增最后数组里存的就是每个字符的出现次数。#include stdio.h int main() { char s[1000]; int cnt[256] {0}; printf(请输入字符串); fgets(s, sizeof(s), stdin); for (int i 0; s[i] ! \0 s[i] ! \n; i) { cnt[(unsigned char)s[i]]; } for (int i 0; i 256; i) { if (cnt[i] 0) { printf(字符 %cASCII %d%d 次\n, i, i, cnt[i]); } } return 0; }很多教材里的写法是cnt[s[i]]直接拿char类型当数组下标这在绝大多数情况下没问题但要留一个心眼C 语言里char类型有没有符号是由编译器决定的。如果char是有符号的字符串里出现中文、扩展 ASCII 码这类高位字节字符时转换成整数会是负数访问负数下标就会越界。所以稳妥的写法是先把s[i]强制转换成unsigned char再作为下标使用。这个细节在 OJ 上见过不少人翻车明明本机跑得好好的一提交就“数组越界”或“段错误”。2.3 再往前走一步单词统计和子串统计四类统计和频率统计做完之后这一题还可以继续扩展成“统计单词数量”。思路是用状态标记当前是否处于“单词内部”遇到空格、制表符、换行就切换状态。#include stdio.h #include ctype.h int main() { char s[1000]; int words 0, inWord 0; fgets(s, sizeof(s), stdin); for (int i 0; s[i] ! \0 s[i] ! \n; i) { if (isspace(s[i])) { inWord 0; } else { if (!inWord) { words; inWord 1; } } } printf(单词数%d\n, words); return 0; }这种“用状态变量记录上下文”的思路在字符串题里很重要。比如统计子串出现次数就需要一个当前位置指针不断在主串里查找子串找到一次就计数一次然后越过这个子串继续往后找。3. 隐藏口令抓到“口令嵌入规则”题目就解了一大半第93题“隐藏口令”真正折磨人的地方就是题目描述里经常不直接告诉你口令是怎么藏的。如果你按常规思路硬着头皮遍历扫描会发现自己不知道要扫描什么。我实际的做题顺序是这样的先穷举题目可能考查的规则再用小样本人工验证最后写代码。3.1 题型A按字符类型筛出大写字母或数字这是最简单的“隐藏”方式一段乱七八糟的字符串里口令就是卷在所有字符里的大写字母。场景类似于“请提取以下字符串中所有的大写字母作为口令”。#include stdio.h #include ctype.h #include string.h int main() { char s[1000], pwd[1000] ; int k 0; printf(请输入字符串); fgets(s, sizeof(s), stdin); for (int i 0; s[i] ! \0 s[i] ! \n; i) { if (isupper(s[i])) { pwd[k] s[i]; } } pwd[k] \0; printf(隐藏口令%s\n, pwd); return 0; }这个解法背后的逻辑是先定义“口令字符”的类型条件然后遍历扫描符合条件就拼到结果字符串末尾。如果把大写字母换成数字字符那判断条件就改成isdigit(s[i])如果口令还要求不能重复那就在拼接前先查一下结果字符串里是否已经存在该字符。3.2 题型B按固定下标抽取字符另一种常见规则是按位置取比如“从第2个字符开始每隔3个字符取一个”最终拼成口令。#include stdio.h #include string.h int main() { char s[1000], pwd[1000] ; int k 0; fgets(s, sizeof(s), stdin); int len strlen(s); for (int i 2; i len s[i] ! \n; i 3) { pwd[k] s[i]; } pwd[k] \0; printf(隐藏口令%s\n, pwd); return 0; }这类题真正的考点是你能不能看懂“起点、步长、终止条件”这三个参数。还有一个变体是“取每行的第 N 个字符”这种情况通常配合多行输入一起出现需要逐行读取、逐行取字符、最后拼接成完整口令。提示如果题目要求从多行字符串中取口令别在一开始就把所有行拼成一个大字符串再遍历。逐行处理更省内存也更符合题目的“制造口令”逻辑。3.3 题型C简单移位加密需要解密如果口令不是明文而是一段密文那就得考虑凯撒移位这类简单加密。规则是每个字母按照字母表向后或向前移动若干位。解密时做逆运算即可。#include stdio.h #include ctype.h #include string.h void decrypt(char *s, int shift) { for (int i 0; s[i] s[i] ! \n; i) { if (isupper(s[i])) { s[i] (s[i] - A - shift 26) % 26 A; } else if (islower(s[i])) { s[i] (s[i] - a - shift 26) % 26 a; } } } int main() { char s[1000]; int shift; printf(请输入密文); fgets(s, sizeof(s), stdin); printf(请输入移位值); scanf(%d, shift); decrypt(s, shift); printf(解密结果%s\n, s); return 0; }这里的核心是模运算。字母移位绕回字母表首尾时不能直接加或减要用取模把结果限制在 26 个字母范围内。26是为了防止- shift后出现负数取模保证结果落在 0 到 25 之间再加上A或a的 ASCII 码得到正确字符。3.4 一套通用的“找口令”解法流程做完几个版本后我把这类题的解法沉淀成了一套流程读题时先找“隐藏规则”的关键词比如“大写字母”“每隔”“右移”“倒序”。根据规则确定筛选条件是字符类型判断、下标取样还是字符变换。决定扫描方式是单指针遍历还是双指针以及遍历的起点和步长。决定结果存储方式一般用定长字符数组边筛边拼最后补\0。人工用手写一个小例子验证规则再跑代码。很多时候你觉得自己“没思路”其实是没有完成第 1 步。规则一旦明确后面四步就是标准的体力活。4. 这两题背后的字符串基本功比题目本身更值得总结第92题和第93题做下来我发现它们真正抽查的并不是什么高深算法而是几个最基本的字符串处理能力。把这些基本功练扎实了以后看到字符串排序、字符串分割、字符串替换这类题思路会顺很多。4.1 先建立“字符串操作原子”清单我习惯把字符串题拆成一个个不可再分的操作原子遍历逐个访问字符这是所有字符串题的地基。查找在字符串里找某个字符或子串C 里用strchr、strstr。截取取字符串的某一段需要自己控制下标并手动拼\0。拼接把多个字符串或字符合成一个用strcat或手动维护下标。比较判断两个字符串是否相等C 里用strcmp不能用。分割按空格或某个分隔符拆分字符串strtok很常用但要小心它会修改原字符串。替换把字符串里某个字符或子串替换成另一个手写时要处理好长度变化。反转字符串逆序经典的双指针交换头尾字符。大小写转换toupper/tolower或者利用 ASCII 码相差 32 的性质。字符串与数字互转atoi、strtol、snprintf也可以用自己写的循环累加。这一串操作里字符串统计考的其实是“遍历 计数”隐藏口令考的是“遍历 筛选 拼接”。很多看起来很难的字符串题本质就是这些原子操作的不同排列组合。4.2 C 语言与 Python 的常用函数对照刷题时我经常在 C 和 Python 之间切换。如果你也用多种语言可以参考下面这个对照表方便快速把思路换成代码。操作C 语言常用方式Python 常用方式字符串长度strlen(s)len(s)遍历字符s[i]for ch in s截取子串手动复制 \0s[start:end]查找字符/子串strchr/strstrs.find(x)字符串比较strcmp(a, b)a b字符串分割strtoks.split(sep)字符串替换手动实现s.replace(old, new)字符串逆序双指针交换s[::-1]字符串转数字atoi/strtolint(s)字符大小写toupper/tolowerch.upper()/ch.lower()Python 因为字符串是不可变对象做拼接时用列表收集再join会更高效这跟 C 里不断strcat造成的重复拷贝是同一类问题。刷题时如果发现某个操作特别别扭不妨换个语言试试往往能更好地理解这个操作的本质。4.3 双指针和滑动窗口什么时候用得上字符串统计和隐藏口令本身未必需要双指针但同一考点的进阶题就见了比如“最长不重复子串”要用滑动窗口回文判断要用双指针从两端向中间逼近字符串分割的经典写法里也要维护快慢两个指针。我个人建议在掌握基础遍历之后优先把双指针练熟因为它能解决大量字符串和数组问题。双指针的核心思想很简单一个循环里维护两个下标一个跑得快一个跑得慢或者一个从头一个从尾根据条件移动其中一个。它能把很多需要两层循环的问题降成一层循环时间复杂度从 O(n²) 降到 O(n)。5. 实测复盘输入输出、字符编码和调试里的三个坑代码写完之后真正让我多花时间的是调试阶段。这里分享三个我在实测中踩过的坑都是网上教程很少提到的细节。5.1 读字符串的三种方式后果完全不一样想读取带空格的字符串最稳妥的是fgets它会读入换行符需要手动处理。scanf(%s, s)遇空格就停适合读单词。gets(s)虽然能读整行但不检查缓冲区长度存在溢出风险新标准里已经被移除了不建议再用。排查这类问题有一个很笨但很有效的方法写一个临时循环把每个字符和它的 ASCII 码一起打印出来。你马上就能看到缓冲区里到底有没有多余的\n哪个字符被误判成了空格一目了然。for (int i 0; s[i] ! \0; i) { printf(s[%d] %d\n, i, (unsigned char)s[i]); }5.2 字符当数组下标时的有符号陷阱前面已经提过char在部分编译器下等同signed char取值范围是 -128 到 127。当字符串中出现高位字符大于 127时直接转成数组下标会变成负数轻则数组越界重则程序崩溃。处理办法是统一转(unsigned char)或者把计数数组开到更大并使用安全的索引计算方式。此外判断字符类型时优先用ctype.h的isalpha、isdigit、isupper等函数它们内部已经处理了符号问题。手动比较 ASCII 码范围虽然也能用但容易漏掉边角情况。5.3 中文字符串、换行符和 EOF 的判定如果你在 OJ 上写这类题输入里混入中文或 UTF-8 编码的字符串问题会复杂一些。UTF-8 编码下一个汉字占 3 个字节每个字节都在 0x80 到 0xFF 之间逐字节处理时不控制好下标非常容易拆散一个完整的汉字导致统计结果错乱。这类情况我会分场景处理如果题目允许优先按宽字符处理如果只能用字节就对每个字节做 ASCII 判断并把高位字节统一归类为“其他字符”。不要试图逐字节去“认识”汉字。5.4 调试时先跑最小样例再扩大测试字符串题最容易在边界条件翻车空字符串、只有一个字符、全是同一个字符、首尾是空格、最后一行没有换行符。我每次写完代码一定会用这几类输入过一遍。如果你总觉得代码逻辑没问题但输出不对就去验证边界样例。一个小技巧把中间变量的状态打印出来。找口令的题我会在循环里打印当前被选中的字符和已经拼接好的口令统计类的题我会打印计数器的实时值。这样能快速定位到底是筛选条件不对还是拼接顺序有问题。最后再分享一个我自己的做题习惯刷完这两道题我最大的收获不是代码本身而是一个处理“没思路”的固定套路先把题目描述里的动词圈出来是“统计”“提取”还是“转换”然后把输入输出样例摆在一起对照最后穷举几种常见规则手工模拟一遍。这样做之后即使题目描述再怎么绕你也能抓住那个真正的考点。如果你也遇到类似“两个没啥思路”的题别急着看题解先尝试自己把题目拆成输入、规则、输出三段。拆明白了你可能会发现它比想象中简单得多。