






🌟 编程题2:分割字符串
它表面上是在考"字符串",但实际上把几个非常重要的 C++ 知识点串在了一起:
-
string -
getline() -
find() -
字符串下标
-
for循环 -
break -
连续空格的处理
-
"每次处理完,再处理剩下的字符串"
这道题最重要的思想不是某一行代码,而是:
像切蛋糕一样,一刀一刀切;每切一次,下一刀需要的连续空格就增加1个。
题目规定:第一次遇到 1个连续空格 就分割;第二次需要 2个连续空格 ;第三次需要 3个连续空格,依次增加,直到无法继续分割。每次输出这次分割的左半部分,最后再输出最后剩下的字符串。
一、先讲一个"小明切字符串"的故事
假设小明得到一条字符串:
sdfd 55 55 99312
注意!
这里的空格不是装饰品,它们可是这道题的"刀"。
我们先给空格编号:
sdfd 55 55 99312
↑
1个空格
第一次要求:
找到第一个 1个连续空格。
所以第一次从这里切:
sdfd |55 55 99312
输出左边:
sdfd
剩下右边:
55 55 99312
二、第二刀更加严格
第一次切完以后,第二次要求:
必须找到 连续2个空格。
剩下:
55 55 99312
注意前面的:
55 55
中间只有:
1个空格。
不够!
但是:
55 55···99312
↑↑↑
3个空格
这里有3个连续空格。
既然:
3个连续空格里面当然包含2个连续空格。
所以第二次可以切:
55 55 | 99312
注意第二次切掉的是:
两个空格
于是:
左边 = 55 55
右边 = 99312
这里右边还会留下 1个空格。
这正是题目特别提醒的地方:第二次只需要两个空格,而原来这里有三个,所以会留下一个空格。
三、为什么第三次不能继续?
第三次要求:
必须找到 连续3个空格。
现在剩下:
99312
最前面只有:
1个空格
没有3个连续空格。
所以:
❌ 第三次无法分割。
最后输出剩下的:
99312
注意这个字符串前面还有一个空格!
四、所以整个过程就像这样
原字符串:
sdfd 55 55 99312
↑
第1次:找1个空格
↓
sdfd | 55 55 99312
输出:
sdfd
↓
剩下:
55 55 99312
↑↑
第2次:找2个连续空格
↓
55 55 | 99312
输出:
55 55
↓
剩下:
99312
↓
第3次需要3个连续空格
❌ 找不到
↓
最后输出:
99312
因此输出就是:
sdfd
55 55
99312
其中最后一行前面实际上有一个空格。题目样例解释明确说明了这个细节。
五、这道题最难的地方:空格也是字符串的一部分
很多刚学 C++ 的小朋友看到:
sdfd 55 55 99312
会觉得:
"不就是几个空格吗?"
错!
对于字符串来说:
"abc"
和:
"abc "
不是同一个字符串。
前者长度:
3
后者长度:
4
最后那个空格也是一个字符。
所以这道题里面:
⭐ 空格不是"看不见的东西",空格也是字符!
六、为什么不能用 cin >> s?
这是本题第一个非常重要的陷阱。
如果写:
string s;
cin >> s;
输入:
sdfd 55 55 99312
得到的可能只有:
sdfd
因为:
cin >> s;
遇到空格就认为:
"一个单词结束了!"
七、这道题必须用 getline
所以参考程序写:
string s = "";
getline(cin, s);
getline() 的特点就是:
一整行全部读进来,包括中间的空格。
例如输入:
Hello World C++
使用:
getline(cin, s);
得到:
Hello World C++
中间的空格全部保留。
所以记住:
🧠 cin >> s
读一个"单词"。
🧠 getline(cin, s)
读"一整行"。
八、接下来是本题真正的魔法:find()
官方参考程序里面有:
int pos = s.find(spaces);
这是本题最值得掌握的一个函数。
find() 的意思就是:
🔍 在字符串中寻找另一个字符串第一次出现的位置。
比如:
string s = "hello world";
执行:
s.find("world")
就会找到:
hello world
↑
返回 6。
为什么是6?
因为字符串下标从0开始:
字符: h e l l o w o r l d
下标: 0 1 2 3 4 5 6 7 8 9 10
所以:
world
↑
位置6
九、这道题有一个特别聪明的办法
题目要求:
第一次找:
一个空格
第二次找:
两个空格
第三次找:
三个空格
那么我们可以让程序自己制造一个字符串:
string spaces = "";
第一次:
" "
第二次:
" "
第三次:
" "
第四次:
" "
......
这就是参考程序中的:
for (int k = 1;; ++k) {
string spaces = "";
for (int i = 0; i < k; ++i) {
spaces += " ";
}
十、这里的 k 是什么意思?
for (int k = 1;; ++k)
可以把 k 想象成:
🗡️ 第几把越来越长的刀
第一次:
k = 1
需要:
" "
一个空格。
第二次:
k = 2
需要:
" "
两个空格。
第三次:
k = 3
需要:
" "
三个空格。
所以:
k
↓
需要的连续空格数量
1 → " "
2 → " "
3 → " "
4 → " "
......
十一、这个 for 为什么没有条件?
你可能注意到了:
for (int k = 1;; ++k)
正常我们见过:
for (int i = 0; i < n; i++)
但是这里:
for (int k = 1;; ++k)
中间没有条件!
这意味着:
for 无限循环
相当于:
while (true)
也就是说:
k = 1
↓
k = 2
↓
k = 3
↓
k = 4
↓
k = 5
↓
......
什么时候停?
不是靠 for 的条件停,而是程序里面遇到:
break;
才停。
十二、制造"空格钥匙"
看这段:
string spaces = "";
for (int i = 0; i < k; ++i) {
spaces += " ";
}
假如:
k = 3
一开始:
spaces = ""
第一次:
spaces = " "
第二次:
spaces = " "
第三次:
spaces = " "
最终:
spaces = " "
于是:
s.find(spaces)
就相当于:
s.find(" ")
也就是:
🔍 "帮我找三个连续空格!"
十三、找到以后,pos 是什么?
程序:
int pos = s.find(spaces);
假设:
s = "sdfd 55 55 99312"
第一次:
spaces = " "
那么:
pos = s.find(" ");
找到:
sdfd·55·55···99312
↑
所以 pos 就是这个空格的位置。
我们可以把:
pos
理解成:
📍 "刀子应该切在哪里"
十四、然后输出左边
参考程序:
for (int i = 0; i < pos; ++i) {
cout << s[i];
}
cout << endl;
这是什么意思?
假设:
pos = 4
那么:
for (int i = 0; i < 4; ++i)
依次输出:
s[0]
s[1]
s[2]
s[3]
也就是:
s d f d
输出:
sdfd
十五、为什么不输出 s[pos]?
因为:
s[pos]
就是那个用于切割的空格。
题目要求:
用于分割的空格要舍弃。
所以:
i < pos
刚好只输出空格前面的内容。
十六、最有意思的一步:把已经切掉的左边扔掉
程序:
string rest = "";
for (int i = pos + k; i < s.length(); ++i) {
rest += s[i];
}
s = rest;
这段代码非常重要。
我们刚才找到的是:
sdfd······55 55 99312
↑←左边→↑↑↑←右边→
如果:
pos = 4
k = 1
那么:
pos + k
就是:
4 + 1 = 5
也就是说:
从第5个字符开始,重新建立一个字符串。
十七、为什么是 pos + k?
因为:
pos
是连续空格的起始位置。
而:
k
是这次要消耗掉的空格数量。
所以:
pos + k
就是:
跳过左边部分 + 跳过本次使用的空格之后,真正的右半部分起点。
例如:
sdfd 55 55 99312
↑
pos
第一次:
k = 1
所以:
pos + k
刚好跳过这个空格。
剩下:
55 55 99312
十八、第二次更加有意思
现在:
s = "55 55 99312"
此时:
k = 2
程序制造:
spaces = " "
然后:
pos = s.find(spaces);
找到:
55 55···99312
↑
注意:
这里实际有三个空格。
但是:
k = 2
只吃掉两个:
55 55··99312
↑↑
所以剩下:
99312
也就是:
一个空格 + 99312
十九、这就是题目的"小陷阱"
很多孩子会想:
"找到三个空格以后,不是应该把三个都删掉吗?"
❌ 不对!
程序找的是:
" "
也就是:
两个连续空格
所以只消耗:
2个
剩下:
1个
这就是题目样例解释中特别强调的地方。
二十、什么时候 break?
程序:
if (pos < 0 || pos >= s.size())
break;
这里是:
"如果找不到需要的空格,就停止切割。"
为什么:
pos < 0
代表找不到?
因为 find() 找不到目标时,会返回一个特殊值 string::npos。
在这个程序里把它放到 int 中,所以参考程序用:
pos < 0
来判断。
对于初学者,可以先记住:
🔍 find() 找不到 → 没找到
于是:
if (pos < 0)
break;
就是:
找不到刀口了,停止切!
二十一、为什么还有 pos >= s.size()?
参考程序写的是:
if (pos < 0 || pos >= s.size()) break;
这是一个保险判断。
正常情况下,如果找到了一个有效位置:
0 <= pos < s.size()
所以:
pos >= s.size()
也意味着这个位置不能作为有效的字符串位置。
对于这道题的主要逻辑,孩子最应该记住的是:
找不到分隔符,就
break。
二十二、最后为什么还要 cout << s?
这是本题另一个很重要的细节。
循环里面输出的是:
每一次成功切割的左边。
但是最后还有一个:
最后一次切割之后留下来的右边部分。
所以循环结束后:
cout << s << endl;
就是把最后剩下的字符串输出。
二十三、把完整程序连起来
参考程序的完整思路可以写成:
#include <iostream>
#include <string>
using namespace std;
int main() {
string s = "";
// 整行读入,空格也保留下来
getline(cin, s);
// k 表示这一次需要几个连续空格
for (int k = 1;; ++k) {
// 制造 k 个连续空格
string spaces = "";
for (int i = 0; i < k; ++i) {
spaces += " ";
}
// 寻找这 k 个连续空格
int pos = s.find(spaces);
// 找不到,就不能继续分割
if (pos < 0 || pos >= s.size())
break;
// 输出本次分割的左边部分
for (int i = 0; i < pos; ++i) {
cout << s[i];
}
cout << endl;
// 保存本次分割之后的右边部分
string rest = "";
for (int i = pos + k; i < s.length(); ++i) {
rest += s[i];
}
// 以后只处理右边剩下的字符串
s = rest;
}
// 最后剩下的字符串
cout << s << endl;
return 0;
}
参考程序的核心逻辑:使用 getline 保留空格,逐步构造 1、2、3...... 个连续空格,用 find 查找,输出左侧,再把右侧保存回 s,直到找不到对应数量的连续空格。
二十四、我们用"程序运行表"完整模拟一次
输入:
sdfd 55 55 99312
第1轮
k = 1
spaces = " "
原字符串:
sdfd 55 55 99312
↑
找到位置:
pos = 4
输出:
sdfd
剩下:
55 55 99312
第2轮
k = 2
spaces = " "
现在:
55 55 99312
↑↑↑
找到连续两个空格。
输出左边:
55 55
消耗两个空格以后:
99312
第3轮
k = 3
spaces = " "
现在:
99312
只有一个空格。
找不到:
" "
所以:
break;
最后
cout << s << endl;
输出:
99312
二十五、再看第二个样例,更容易发现题目的"坑"
第二个样例类似:
AbCd 999
中间有:
三个连续空格
第一次
需要:
1个空格
所以:
AbCd | 999
第一次输出:
AbCd
但是注意:
原来有三个空格。
第一次只吃掉一个。
所以还剩:
999
也就是:
两个空格 + 999
第二次
现在需要:
2个空格
刚好找到了:
999
↑↑
所以第二次分割:
|999
左边是什么?
是:
空字符串
因此必须输出一个:
空行
这也是题目特别强调的地方:第二次分割可以得到空字符串,所以输出中必须保留这一行,不能偷偷省略。
二十六、这个"空字符串"非常值得孩子理解
什么叫:
""
?
就是:
里面一个字符都没有的字符串。
例如:
string s = "";
那么:
s里面什么都没有
长度:
s.length() == 0
但是:
"没有字符"
和:
"不输出这一行"
不是一回事。
如果题目要求输出空字符串这一行:
cout << endl;
仍然要执行。
也就是:
第一行:AbCd
第二行: ← 空行
第三行:999
二十七、这道题其实是一个"递归式思维",
我们可以把它理解成:
第一次:
处理整个字符串
↓
切掉左边
↓
留下右边
第二次:
只处理右边
↓
再切掉左边
↓
留下新的右边
第三次:
只处理新的右边
↓
......
也就是说:
⭐ "每一次只关心剩下的右边。"
这句话非常重要。
程序中的:
s = rest;
就是在做这个事情。
二十八、为什么不需要保存所有字符串?
有的小朋友可能会想:
第一次的左边
第二次的左边
第三次的左边
......
是不是需要很多数组?
其实不需要。
因为题目规定:
输出一次,就可以把这一部分处理掉。
所以程序只需要一直保存:
当前还没有处理的右边
也就是:
s
因此:
原来的s
↓
切割
↓
新的s
↓
再切割
↓
新的s
↓
......
这是一种非常实用的:
"不断缩小问题规模"的思想。
二十九、这道题考的知识点可以整理成一张图
分割字符串
│
┌─────────┼─────────┐
↓ ↓ ↓
getline find string
│ │ │
保留空格 找位置 存文字
│ │ │
└─────────┼─────────┘
↓
for循环
↓
第1次需要1个空格
第2次需要2个空格
第3次需要3个空格
↓
找不到?
↙ ↘
是 否
↓ ↓
break 分割
↓
s = rest
↓
继续
三十、初学者最应该记住的5个知识点
🥇 第一:getline
getline(cin, s);
记忆:
想把整行,包括空格,一口气读进来,就用
getline。
🥈 第二:find
int pos = s.find("abc");
记忆:
🔍 find = 找东西
找到:
返回第一次出现的位置
找不到:
返回特殊的"没找到"
🥉 第三:string
字符串可以保存:
字母
数字
空格
例如:
string s = "A B C";
其中空格也是字符。
🏅 第四:s = rest
这是本题最重要的算法思想:
处理完左边以后,只留下右边,下一轮继续处理。
🏅 第五:break
if (找不到)
break;
意思:
🛑 "没有刀口了,停止切割!"
三十一、最后给大家一个"分割字符串口诀"
把这道题记成:
🗡️ 第一刀找一个,第二刀找两个,第三刀找三个;
🔍 find 找刀口;
✂️ 输出左边;
🧹 扔掉已经切掉的部分;
👉 留下右边继续切;
🛑 找不到就停止;
📢 最后把剩下的右边输出。
程序思想就是:
getline
↓
k = 1
↓
制造 k 个空格
↓
find 找位置
↓
找不到 → break
↓
输出左边
↓
留下右边
↓
k++
↓
继续
🌟 三十二、这道题我们学到了什么?
如果只记住:
s.find(...)
其实还不够。
这道题真正值得学的是一个非常重要的编程思想:
一个复杂的问题,不一定要一次全部解决,可以先解决一小部分,然后把问题变小,再解决剩下的部分。
就像爬楼梯:
整栋楼
↓
先上一层
↓
剩下的楼梯
↓
再上一层
↓
剩下的楼梯
↓
......
↓
到达终点
这就是算法中非常重要的:
🧠 "不断处理、不断缩小问题"
而这道题恰好用字符串,把这个思想演示得非常清楚。
如果孩子把这道题真正吃透,那么将来学习双指针、模拟、递归,甚至一些更复杂的算法时,都会发现:很多题其实都是在想办法让"当前的问题越来越小"。