中 · 字符串状态

数单词

第一道需要你记住上一个字符才能判断当前字符含义的题。这叫状态机,它无处不在。

一句话答案单词是任意一段连续的非空格字符,所以数「从空格进入非空格」的次数 —— 必须用 getline 读整行(cin >> 会在第一个空格处停下),并且连续空格不能算成空单词。

题目

输入: 一行文本,最长 200 个字符。可能包含字母、标点和空格 —— 包括行首行尾的空格,以及连续多个空格。

输出: 单词个数。一个单词就是一段连续的非空格字符。

样例:

输入输出
hello world2
a b c 3
0

要用 cin.getline(s, size) 读,不能用 cin >> s。后者遇到第一个空格就停,任何输入都只会读到一个单词。

你的解答

main.cpp — 你的解答
「运行我的输入」用的自定义输入

判题在浏览器本地运行。死循环会在 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;
}