中 · 字符串状态
数单词
第一道需要你记住上一个字符才能判断当前字符含义的题。这叫状态机,它无处不在。
一句话答案单词是任意一段连续的非空格字符,所以数「从空格进入非空格」的次数 —— 必须用 getline 读整行(cin >> 会在第一个空格处停下),并且连续空格不能算成空单词。
题目
输入: 一行文本,最长 200 个字符。可能包含字母、标点和空格 —— 包括行首行尾的空格,以及连续多个空格。
输出: 单词个数。一个单词就是一段连续的非空格字符。
样例:
| 输入 | 输出 |
|---|---|
hello world | 2 |
a b c | 3 |
| 0 |
要用 cin.getline(s, size) 读,不能用 cin >> s。后者遇到第一个空格就停,任何输入都只会读到一个单词。
你的解答
「运行我的输入」用的自定义输入
判题在浏览器本地运行。死循环会在 3 秒后被自动中断 —— 但还是确保每个循环都有出口。代码随打随存。
卡住了?提示
提示 1 — 别数单词,数单词的<em>开始</em>
逐字符走的时候,你看不到完整的单词。但你能抓到一个单词开始的瞬间:当前字符不是空格,而前一个字符是空格。每一个「开始」就是一个单词。
提示 2 — 把「上一个是不是空格」变成一个变量
bool inWord = false; —— 我现在是不是在单词内部?遇到空格设为 false;遇到非空格时,如果它原来是 false,说明刚发现一个新单词:计数器加一,再设为 true。这一个标志同时解决了行首空格、行尾空格和连续十个空格。
提示 3 — 那个很诱人的错误答案
数空格个数再加一。这在 hello world 上成立,但在任何有行首空格、行尾空格或连续两个空格的输入上都会挂 —— 而现实文本大多如此。这是「捷径暗含了对输入的假设」的典型例子。
题解(先自己试试!)
#include <iostream>
using namespace std;
int main() {
char s[205];
cin.getline(s, 205);
int count = 0;
bool inWord = false;
for (int i = 0; s[i] != '\0'; i++) {
if (s[i] == ' ') {
inWord = false;
} else if (!inWord) {
inWord = true;
count++;
}
}
cout << count << endl;
return 0;
}
把 s[i] == ' ' 换成 !isalpha(s[i]),你就得到一个忽略标点的单词计数器。换成「是不是分隔符列表里的字符」,你就得到一个 CSV 解析器。这个模式 —— 用一个布尔值描述「我现在在哪」,边扫边更新 —— 正是编译器做词法分析、编辑器做语法高亮、HTTP 解析器找头部边界的方式。接下来让数组自己重排:冒泡排序。
深度解析
为什么这样做对
逐字符扫的时候你看不到完整单词,但能抓到「一个单词刚开始」的那个瞬间:当前字符不是空格,而前一个字符是。一个布尔值就够。
代价
O(长度),只多一个 bool。而「数空格再加一」写起来更省事,却在行首空格、行尾空格、连续两个空格上全错 —— 你拿正确性换了那点代码量。
常见错法
- 用
cin >> s读:遇到第一个空格就停,任何输入都只读到第一个单词。 - 数空格个数再加一:行首空格、行尾空格、连续两个空格,三种情况一起挂。
下一步
同一个状态机骨架,换个分隔符就是 CSV 解析器。接下来让数组自己重排:冒泡排序。
记住这两句
- 别数单词,数单词的「开始」。
- 一个布尔值描述「我在哪」,边扫边更新 —— 这就是状态机。
🐞 这段是错的 —— 点开,亲手让它挂一次
数空格再加一 —— 行首、行尾、连续空格全错
#include <iostream>
using namespace std;
int main() {
char s[205];
cin.getline(s, 205);
int count = 1;
for (int i = 0; s[i] != '\0'; i++) {
if (s[i] == ' ') count++;
}
cout << count << endl;
return 0;
}