十、字符串

这是最后一页,5 章 33 道题。

字符串题的特点是:逻辑不难,细节极多。 算法你可能一眼就想到,但空格、换行、大小写、末尾的 \0、scanf 读到哪停下……每一处都能让你 WA 半天。

所以这一页的重点不是「想不出来怎么做」,而是「想到了但写不对」。33 道题,大部分失分都在细节上。

先把最要紧的三件事记住:

  1. 字符数组的结尾有一个 \0,它不算内容,但占一个位置
  2. scanf("%s") 遇到空格就停,读不了带空格的一整行
  3. 字符串不能直接用 == 比较,要用 strcmp

31. 字符与字符串基础

31.1 char 数组与 string

C++ 有两种字符串,初学阶段建议先用 char 数组:

char s[105];                  // 能装 104 个字符 + 结尾的 '\0'
scanf("%s", s);               // 注意:不加 &
printf("%s\n", s);

%s 读入时,s 前面不加 &——因为数组名本身就是地址。加了反而错。

另一种是 C++ 的 string 类型:

string s;
cin >> s;                     // string 要用 cin 读
cout << s << endl;

string 更方便(能直接 =、+、==),但它不能用 scanf 读。既然全站用 scanf,那就用 char 数组,一以贯之。

一个字符数组装 n 个字符,至少要开 n+1 个位置,因为末尾要放一个 \0(字符串结束标志)。\0 的码值是 0,不是字符 '0'。

char s[6] = "hello";
// s[0]='h' s[1]='e' s[2]='l' s[3]='l' s[4]='o' s[5]='\0'

数组开小了会「吃掉」后面的变量

char s[5];
scanf("%s", s);      // 输入 "hello" 是 5 个字符,但需要 6 个位置

5 个字符 + \0 = 6 字节,写进了只有 5 字节的空间。多出来的那个字节会覆盖掉别的变量。

字符串数组一律多开,题目说长度 ≤ 100,就开 105 或 110。

字符串的读写

346 这类题看着简单,但如果输出格式要求「每个字符之间加空格」之类,就得用循环逐字符输出,不能直接 printf("%s", s)。

31.2 读一整行

scanf("%s", s) 遇到空格或换行就停。所以输入 hello world,它只读到 hello。

要读带空格的一整行,用 fgets:

char s[105];
fgets(s, sizeof(s), stdin);

三个参数:装到哪、最多读多少、从哪读(stdin 是标准输入)。

fgets 会把行末的换行符也读进来。 所以读完之后通常要去掉:

int len = strlen(s);
if (len > 0 && s[len-1] == '\n') s[len-1] = '\0';

这一步不做的话,你输出的内容末尾会多一个空行,或者字符串比较时永远不相等。

另一种写法是 scanf("%[^\n]", s),意思是「读到换行为止」:

scanf(" %[^\n]", s);     // 前面那个空格很重要,用来跳过上一行留下的换行

%[^\n] 是「字符集合」的写法,^\n 表示「除了换行以外的所有字符」。

fgets 和 scanf 混用会串行

int n;
scanf("%d", &n);         // 读完数字,回车还留在缓冲区
fgets(s, sizeof(s), stdin);   // 读到一个空行!

第 3 章 %c 的那个坑,在这里以另一种形式出现了。scanf("%d") 只吃掉数字,不吃回车。

解决办法:在 fgets 之前先「吃掉」那个回车,或者用 scanf(" %[^\n]", s)(前面加空格)。

判断方法:如果 fgets 读到的第一行是空的,就是这个原因。

读整行

345 的难点在「怎么切词」。一种做法是遍历字符,遇到空格就认为一个词结束;另一种是用 scanf("%s") 连续读,它会自动跳空格——但这样读不到「第 k 个」之外的信息。

31.3 遍历与修改

遍历字符串,就是遍历到 \0 为止:

for (int i = 0; s[i] != '\0'; i++) {
    // 处理 s[i]
}

或者先求长度:

int len = strlen(s);        // 需要 #include <cstring>,bits 里有
for (int i = 0; i < len; i++) {
    // 处理 s[i]
}

两种写法等价。第一种少算一次长度,第二种可以在循环里用 len。

strlen 的复杂度是 O(n)——它要从头数到 \0。所以不要在循环条件里反复调用 strlen(s):

for (int i = 0; i < strlen(s); i++)    // 错:每次都重算一遍,变成 O(n²)

正确做法是先存下来:int len = strlen(s);。

逐字符处理

348 别用 isdigit() 也行,直接比较码值范围更直观。'0' 到 '9' 是连续的。

31.4 大小写转换

char c = 'a';
char upper = c - 'a' + 'A';    // 'A'
char lower = 'A' - 'A' + 'a';  // 'a'

因为大小写字母各自的码值是连续的,所以「减掉小写起点,加上大写起点」就能转换。

也有现成的函数:

toupper(c);      // 转大写,如果是小写的话
tolower(c);      // 转小写

toupper 对非字母字符原样返回,所以不用先判断。

大小写

360 要两个方向都判断,不能只写一半。写完拿 aA1 这组输入测一下,看看数字有没有被误改。

第 31 章验收

  • 知道 char s[105] 里最多装 104 个字符
  • 会用 fgets 读整行,并且知道要去掉末尾换行
  • 知道 scanf("%s") 遇空格就停
  • 遍历字符串用 s[i] != '\0',且不会在循环条件里调 strlen

32. 遍历、统计与判定

32.1 用遍历做判定

「这个字符串是不是合法的 C 标识符」「两个字符串有百分之多少相同」——本质都是遍历一遍,逐字符检查。

bool ok = true;
for (int i = 0; s[i]; i++) {
    if (!(isalnum(s[i]) || s[i] == '_')) {    // 不是字母数字下划线
        ok = false;
        break;
    }
}

isalnum(c) 判断是不是字母或数字,isalpha(c) 判断字母,isdigit(c) 判断数字。都在 <cctype> 里,bits 已包含。

注意 C 标识符不能以数字开头——这个条件要单独判断 s[0]。

判定

32.2 配对与镜像

「碱基配对」这类题:A 配 T、C 配 G。写法就是逐字符判断后输出对应的字符。

for (int i = 0; s[i]; i++) {
    if (s[i] == 'A') printf("T");
    else if (s[i] == 'T') printf("A");
    else if (s[i] == 'C') printf("G");
    else if (s[i] == 'G') printf("C");
}

如果配对规则很多,用两个数组做映射表更清爽:

char from[] = "ATCG";
char to[]   = "TAGC";
for (int i = 0; s[i]; i++) {
    for (int j = 0; from[j]; j++) {
        if (s[i] == from[j]) { printf("%c", to[j]); break; }
    }
}

配对

32.3 回文判断

从头尾两头往中间比:

int len = strlen(s);
bool ok = true;
for (int i = 0; i < len / 2; i++) {
    if (s[i] != s[len - 1 - i]) {
        ok = false;
        break;
    }
}

len / 2 是重点——只需要比一半,全部比完是浪费。奇偶都不用特殊处理:len = 5 时 len/2 = 2,比 s[0] 和 s[4]、s[1] 和 s[3],中间那个 s[2] 自己和自己相等,不用比。

回文

  • 回文判断 —— 注意题目是否要求「忽略大小写」或「忽略空格」

如果要忽略大小写,比较前先 tolower 两边。

32.4 用计数数组统计字符

字符的码值范围是 0~127,可以开一个 128 大小的数组当桶:

int cnt[128] = {0};
for (int i = 0; s[i]; i++) cnt[s[i]]++;
// cnt['a'] 就是 'a' 出现的次数

这是桶的思想在字符上的应用——用字符的码值当下标,第 23 章学过。

字符统计

两遍循环:第一遍统计,第二遍从头找第一个「次数为 1」的。顺序就是原字符串的顺序,别排序。

第 32 章验收

  • 会用遍历 + if 做字符判定
  • 回文判断知道只比一半
  • 会用 int cnt[128] 统计字符频次

33. 字符映射与加解密

这一章的所有题,本质都是把字符按某个规则换成另一个字符。规则不同,套路一样。

33.1 偏移:凯撒密码

每个字母往后移 k 位,超出 z 就回到 a。

char shift(char c, int k) {
    if (c >= 'a' && c <= 'z') return (c - 'a' + k) % 26 + 'a';
    if (c >= 'A' && c <= 'Z') return (c - 'A' + k) % 26 + 'A';
    return c;                    // 非字母原样返回
}

第 6 章讲过这个套路:减掉起点 → 做运算 → 加回起点。% 26 负责回绕。

注意非字母字符要原样保留(空格、标点、数字)。很多题就靠这一点设坑。

凯撒类

33.2 大小写混合的规则

有些题要求「大写转小写,小写转大写,其他不变」,还有些要求「只处理字母,非字母跳过」。

if (c >= 'A' && c <= 'Z') c = c - 'A' + 'a';
else if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';

用 if / else if 而不是两个独立的 if——先转成小写再判断是不是小写,会转两次,结果又变回大写。第 9 章讲的「独立 if 和 else if 的区别」,在这里会真实咬人。

大小写与整理

361 的坑:如果原来是全大写,转完要保证只有一个大写。

33.3 建立映射表

「潜伏者」这类题,给一张密码表和对应的明文,要你推出整个映射关系。

这种题的通用做法是开两张表:

char enc[26], dec[26];    // enc[明文] = 密文, dec[密文] = 明文
bool used[26] = {false};

然后:

  1. 遍历样例,把每一对「明文-密文」存进两张表
  2. 检查有没有矛盾(同一个明文字母对应了两个不同密文)
  3. 检查 26 个字母是不是都被映射了

这类题难在逻辑,不在字符串操作。 先把「什么情况下算冲突」「什么情况下算不完整」想清楚,再写代码。

映射表

建议先在纸上列出所有可能的冲突情况,再写代码。直接写很容易漏。

第 33 章验收

  • 会用「减起点、算、加起点」做字母偏移
  • 知道非字母字符通常要原样保留
  • 知道大小写转换要用 else if

34. 比较、子串与循环移位

34.1 字符串比较

不能用 ==——s1 == s2 比较的是两个数组的地址,永远不相等。

要用 strcmp:

if (strcmp(s1, s2) == 0) printf("same\n");

strcmp 返回:

  • 0:两个字符串完全相同
  • 负数:s1 字典序小于 s2
  • 正数:s1 字典序大于 s2

记住只判断 == 0 和 != 0 就够了,具体返回多少不用管。

忽略大小写的比较,先把两边都转成小写再比:

for (int i = 0; s1[i]; i++) s1[i] = tolower(s1[i]);
for (int i = 0; s2[i]; i++) s2[i] = tolower(s2[i]);
if (strcmp(s1, s2) == 0) { ... }

比较

362 注意:转换会修改原字符串。如果后面还要用原串,先复制一份。

34.2 查找子串

char *p = strstr(s, t);      // 在 s 里找 t
if (p != NULL) {
    printf("%d\n", (int)(p - s));   // 找到的位置(从 0 开始)
}

strstr 返回一个指针,指向第一次出现的位置。p - s 就是偏移量。

找不到时返回 NULL(空指针)。

不想用指针的话,手写两层循环也行:

bool found = false;
for (int i = 0; s[i]; i++) {
    bool ok = true;
    for (int j = 0; t[j]; j++) {
        if (s[i+j] != t[j]) { ok = false; break; }
    }
    if (ok) { found = true; break; }
}

子串

366 的「截掉」就是提前放一个 \0:s[len - k] = '\0';。这一招很好用,比新建一个字符串简单。

34.3 循环移位

「判断 s2 是不是 s1 循环移位得到的」有个巧办法:把 s1 接上自己,看 s2 是不是它的子串。

char t[205];
strcpy(t, s);
strcat(t, s);          // t = s + s
if (strstr(t, s2) != NULL) { /* 是循环移位 */ }

比如 s = abcd,接上自己是 abcdabcd,里面包含 bcda、cdab、dabc——正好是所有循环移位。

strcpy 是复制,strcat 是拼接。两个都要保证目标数组够大(这里是 205,因为要装两倍长度)。

循环移位

这两题的「接一遍自己」是同一个思路。凡是「环形」「循环」的字符串题,先想能不能复制一遍变成线性问题。

34.4 多分支列举

列举所有情况

这题有 9 种组合,但可以化简:如果相等是平局;否则看是谁赢了谁。写成 if (s1=="石头" && s2=="剪子") ... 会很啰嗦,考虑用编号代替字符串比较。

第 34 章验收

  • 知道字符串不能用 ==,要用 strcmp
  • 会用 strstr 找子串
  • 会用「提前放 \0」来截断字符串
  • 知道循环移位可以「复制一遍」变成子串问题

35. 单词处理与综合

这一章的题都是「处理一句话」——切词、替换、统计。

35.1 按空格切词

最朴素的做法:

for (int i = 0; s[i]; i++) {
    if (s[i] != ' ') {
        word[wlen++] = s[i];       // 累积到缓冲区
    } else if (wlen > 0) {
        word[wlen] = '\0';         // 一个词结束
        处理(word);
        wlen = 0;                  // 清空
    }
}
if (wlen > 0) { word[wlen] = '\0'; 处理(word); }   // 处理最后一个词

注意两件事:

  1. 连续多个空格要跳过(wlen > 0 那个判断)
  2. 循环结束后最后一个词要单独处理(因为它后面没有空格)

第 2 点是最高频的错——最后一个词被吃掉。

另一种更简单的方式:用 scanf("%s") 连续读,它自动跳过所有空格。

char word[105];
while (scanf("%s", word) == 1) {
    // 处理 word
}

scanf 返回成功读入的项数,读到文件末尾返回 EOF(-1)。这个写法在处理「不知道有多少个词」时特别好用,而且不用管多个空格的问题。

切词

369 用逐字符处理更容易控制空格;370 用 scanf("%s") 循环更省事。

35.2 单词替换与倒排

替换单词的坑在于:不能直接找子串替换。

比如把 he 替换成 she,字符串是 the hero——直接找 he 会命中 the 里的 he,变成 tshe shero。

正确做法是先切词,按词判断:

while (scanf("%s", word) == 1) {
    if (strcmp(word, target) == 0) printf("%s ", replacement);
    else printf("%s ", word);
}

倒排更简单:把词读进一个数组,倒着输出。

char w[1005][105];
int n = 0;
while (scanf("%s", w[n]) == 1) n++;
for (int i = n - 1; i >= 0; i--) printf("%s ", w[i]);

单词操作

371 注意末尾不能多一个空格——最后一词后面不要打空格,可以用 if (i) printf(" "); 控制。

35.3 行程长度编码

把连续重复的字符压缩成「字符 + 次数」:aaabbc → a3b2c1。

int i = 0;
while (s[i]) {
    int j = i;
    while (s[j] == s[i]) j++;      // 找到相同字符的结尾
    printf("%c%d", s[i], j - i);   // 字符 + 个数
    i = j;                          // 从下一个字符继续
}

这个「两个指针,一个标记起点、一个往后探」的写法,在字符串题里会反复出现。

编码

很多题会规定「单个字符不写次数」。读题目里的每一句话,这类题的要求往往藏在例子里。

35.4 校验与格式判定

校验

  • ISBN号码 —— 通常是「去掉连字符,算校验位,判断对不对」

这题的步骤要多一些,但每一步都不难:读入 → 提取数字 → 按公式算 → 比较。拆成几个小函数写。

35.5 综合

最后的综合题

376 可能要用到「两个数组比对着遍历」,或者把词排序后比较。先想清楚「相似」的定义是什么,再动手。

字符串板块总结

33 道题做完,你应该达到:

  • 拿到字符串题,第一件事是确认「有没有空格」——决定用 scanf("%s") 还是 fgets
  • 知道 \0 的存在,且数组会多开几个
  • 会用 strcmp、strstr、strlen、strcpy、strcat
  • 切词时不会漏掉最后一个词

全站总结

到这里,35 章 254 道题就走完了。回头看一下你走过的路:

阶段 你学会的东西
基础 程序骨架、输出、算术、printf
计算 scanf、变量、整数/浮点运算、字符与 ASCII
判断 if / else if、逻辑运算、分段与边界
循环 for / while、累加、最值、遍历、递推、模拟
数组 遍历、桶、排序、前缀和、差分
函数 封装、返回值、拆解
结构体 打包数据、多关键字排序
字符串 字符数组、整行读取、切词、映射

这 254 道题不是终点。 它们覆盖的是「能用 C++ 把想法写出来」这个最低门槛。真正难的题,考的是你想不想得到那个思路——而那件事,靠的是更多题的积累。

但有一样东西比题量重要:每一道题,是不是你自己啃下来的。

看着题解抄一遍然后 AC,你得到的是一次「通过」的记录和零能力增长。这道题三天后问你,你还是不会。

延伸阅读

服务器上有一整套 OI-Wiki(621 页),是给竞赛选手写的参考手册。下面几页跟这一页相关,但不是每一页都该现在看——标了「现在跳过」的,等以后再说。

  • 可以先看:string —— 只看开头「string 是什么」。后面 substr、find 那些函数第 34 章才用得上
  • 学完这一页再看:字符串基础 —— 字符串的存储方式。看完能解释 s[i] 为什么能取到第 i 个字符
  • 现在跳过:字符串哈希 —— 比较字符串的竞赛级做法。你现在用 strcmp 就够了,别碰

看不懂是正常的,不是你的问题

OI-Wiki 默认读者已经会写程序、目标是打 NOI。你现在需要的那部分,讲义里已经写了; wiki 是用来「以后查得更深」的,不是用来补课的。

© HiJudge · 内容采用 CC BY-NC-SA 4.0 许可协议            此页面修订于: 2026-11-01 03:44:10

results matching ""

    No results matching ""