LeetCode 434「字符串中的单词数」模拟解法详解:一次遍历完成单词计数(LogicStack-LeetCode 刷穿系列)

  • 教程
  • 文档

【免费下载链接】LogicStack-LeetCode

公众号「宫水三叶的刷题日记」刷穿 LeetCode 系列文章源码

项目地址: https://gitcode.com/gh_mirrors/lo/LogicStack-LeetCode
点击查看 免费下载

本文基于「宫水三叶的刷题日记」LogicStack-LeetCode 仓库中的 434. 字符串中的单词数(简单) 题解展开,围绕「单词 = 连续的非空格字符」这一核心定义,从题目解读、模拟计数实现、逐行代码分析、边界情况到复杂度论证,完整讲解如何用单次遍历、O(1) 额外空间统计字符串中的单词段数量;文末还会串联仓库内同属「模拟」标签的 58. 最后一个单词的长度 与 2047. 句子中的有效单词数,帮助读者建立字符串分词的通用解题框架。

一、题目解读:什么样的字符序列才算一个「单词」

本题是 LeetCode 上的 434. 字符串中的单词数(Number of Segments in a String),难度为「简单」,在仓库中归类于 Index/模拟.md 的「模拟」标签之下,推荐指数四星。

题目的定义非常关键,它与日常语言的「单词」概念有细微差别:

统计字符串中的单词个数,这里的单词指的是连续的不是空格的字符。

也就是说,本题的「单词」是一个纯语法层面的定义:一段连续的、不含空格字符的字符序列即视为一个单词。因此:

  • 标点符号是单词的一部分,"Hello," 算 1 个单词;
  • 数字、字母、符号混排的序列(如 "a-b-c")同样算 1 个单词;
  • 只有空格 ' ' 才是单词之间的分隔符。

题目还给出了一个重要前提:可以假定字符串里不包括任何不可打印的字符。这意味输入字符集是「可打印字符 + 空格」,无需处理 \n、\t 等空白字符带来的歧义,使解法可以聚焦在空格这一个分隔符上。

官方示例:

输入: "Hello, my name is John"

输出: 5

解释: 这里的单词是指连续的不是空格的字符,所以 "Hello," 算作 1 个单词。

二、核心思路:从前往后的一次遍历模拟

由于「单词」被定义为「连续的不是空格的字符」,统计任务就等价于数出字符串中所有「由空格分隔开的非空字符段」的个数。

原题解给出的策略是:

  1. 从前往后扫描字符串 s;
  2. 遇到空格字符时直接跳过(不参与计数);
  3. 遇到非空格字符时,用内层循环一口气走完当前这一整段连续的非空格字符(即一个完整单词),然后计数加一。

这个过程天然处理了前导空格、尾随空格和连续多个空格:因为所有空格都被跳过,只有「完整走过一段非空格字符」才会触发一次计数,所以不会把 " hello world " 之类的输入数出多余的结果。

原题解代码(Java):

class Solution {
    public int countSegments(String s) {
        int n = s.length();
        int ans = 0;
        for (int i = 0; i < n; ) {
            if (s.charAt(i) == ' ' && i++ >= 0) continue;
            while (i < n && s.charAt(i) != ' ') i++;
            ans++;
        }
        return ans;
    }
}

这段代码非常精炼,逐行分析如下:

  • int n = s.length();:记录字符串长度,作为遍历边界;
  • for (int i = 0; i < n; ):外层循环不写自增语句,指针的推进完全交给循环体内的逻辑控制;
  • if (s.charAt(i) == ' ' && i++ >= 0) continue;:这里用了一个短路与 + 后置自增的编码技巧。当 s.charAt(i) 是空格时,i++ >= 0 一定为真(循环条件 i < n 保证了 i >= 0,后置自增返回自增前的值),于是指针 i 前进一步并 continue 跳过该空格;当当前字符不是空格时,i++ 不会执行,逻辑落入内层循环;
  • while (i < n && s.charAt(i) != ' ') i++;:内层循环持续右移指针,直到越过当前单词的最后一个字符;
  • ans++;:走完一整段非空格字符后计数加一。

用示例 "Hello, my name is John" 手动推演一遍(n = 22):

步骤位置 i字符动作ans
10H内层循环消费 Hello,,i 推进到 61
26跳过,i 到 71
37m消费 my,i 到 92
49跳过,i 到 102
510n消费 name,i 到 143
614跳过,i 到 153
715i消费 is,i 到 174
817跳过,i 到 184
918J消费 John,i 到 22,循环结束5

最终返回 5,与题目输出一致。

三、边界情况与正确性验证

单次遍历模拟之所以是这道题的正解,是因为它对所有边界情况都天然正确:

  • 空字符串 s = "":n = 0,外层循环根本不进入,返回 0;
  • 纯空格字符串 s = " ":每个字符都被 continue 跳过,内层循环从不执行,返回 0;
  • 前导空格 s = " hello":开头空格被逐个跳过,然后 hello 被完整消费一次,返回 1;
  • 尾随空格 s = "hello ":hello 计数后剩余空格被跳过,返回 1;
  • 连续多个空格 s = "a b":a 计数一次,三个空格全部跳过,b 再计数一次,返回 2;
  • 单字符 s = "a":内层循环消费完整个字符串,返回 1;
  • 标点粘连 s = "Hello, world":Hello, 因不含空格而算一个单词,与 world 由空格分隔,返回 2。

这些场景可以整理成一组用于本地验证的测试用例(可直接在 LeetCode 提交或本地 JUnit 中运行):

输入期望输出
"Hello, my name is John"5
""0
" "0
" hello world "2
"a"1
"a b c"3
"Hello, world"2

四、复杂度分析

  • 时间复杂度:O(n)。外层指针与内层指针合计将整个字符串扫描一遍,每个字符最多被访问两次(一次作为空格跳过、一次作为单词成员被消费),总体是线性扫描;
  • 空间复杂度:O(1)。全程只使用了 n 和 ans 两个整型变量,没有申请任何与字符串长度相关的额外存储。

这也是该解法相较「先 split 再统计」方案的实质优势:不产生子串数组,常数级额外内存,且不依赖语言内置 API 对「连续空格」「空串」的边界语义。

五、延伸写法:状态切换计数与内建 API 方案

原题解采用「消费整段」的写法,除此之外还有两种常见实现思路,各有取舍,可作为对比学习。

写法二:状态切换计数(单词开头判定)

核心思想是统计「新单词的开头」:当某个字符不是空格,并且它是字符串第一个字符或它前一个字符是空格时,说明一个新单词开始了,计数加一。

class Solution {
    public int countSegments(String s) {
        int ans = 0;
        for (int i = 0; i < s.length(); i++) {
            if (s.charAt(i) != ' ' && (i == 0 || s.charAt(i - 1) == ' ')) {
                ans++;
            }
        }
        return ans;
    }
}

这种写法把「连续段」的思想转化为「相邻字符的状态变化」,同样满足空串、纯空格等边界:空串或纯空格时,s.charAt(i) != ' ' 的条件永远不会成立,返回 0。两种写法时间复杂度一致,写法二的可读性略好,但多了一次对前一个字符的随机访问。

写法三:基于 split 的内建 API 方案

也可以借助语言内置的分割能力,但必须警惕两个经典陷阱:

class Solution {
    public int countSegments(String s) {
        int ans = 0;
        for (String seg : s.split("\\s+")) {
            if (!seg.isEmpty()) ans++;
        }
        return ans;
    }
}
  • 陷阱一:split("\\s+") 对空字符串会返回 [""](长度为 1 的数组),因此必须用 isEmpty() 过滤空串;
  • 陷阱二:连续多个空格虽然会被 \\s+ 合并处理,但如果误用 split(" ")(单个空格),连续空格之间会产生空字符串元素,同样需要过滤。

该写法代码更简洁,但引入了额外数组与子串分配,空间复杂度退化为 O(n);而且在部分语言中正则分割的语义差异较大(如仓库内 2047. 句子中的有效单词数 题解就特别注明:Java 的 split 基于正则、复杂度为线性,其他语言可能需要手写双指针实现 split)。因此面试与竞赛场景下,原题解的纯模拟写法是最稳妥的选择。

多语言对照

原题解给出的是 Java 实现,将该算法平移到其他语言同样直观。以下为忠实于原算法逻辑的 C++ 与 Python 版本:

class Solution {
public:
    int countSegments(string s) {
        int n = s.size(), ans = 0;
        for (int i = 0; i < n; ) {
            if (s[i] == ' ' && ++i >= 0) continue;
            while (i < n && s[i] != ' ') i++;
            ans++;
        }
        return ans;
    }
};
class Solution:
    def countSegments(self, s: str) -> int:
        n, ans, i = len(s), 0, 0
        while i < n:
            if s[i] == ' ':
                i += 1
                continue
            while i < n and s[i] != ' ':
                i += 1
            ans += 1
        return ans

三个版本的执行路径完全一致:跳过空格 → 消费整段 → 计数。

六、仓库联动:字符串分词类模拟题目的串联

本题不是孤立的。在 LogicStack-LeetCode 仓库中,围绕「空格分词」这一主题还有多道同属「模拟」标签的姊妹题,可以放在一起对比学习:

  • 58. 最后一个单词的长度(简单):同样是空格分词,但只关心最后一个单词。解法采用从后往前的双指针扫描:先用指针 j 从末尾跳过尾随空格,再用指针 i 从 j 出发向左越过最后一个单词,最终 j - i 即为答案。它和本题互为镜像——一个正着数段数,一个倒着量长度,但都建立在「单词 = 连续非空格字符」的同一套定义上;
  • 2047. 句子中的有效单词数(简单):在「按空格切分 token」的基础上,进一步对每个 token 做合法性校验(不含数字、至多一个连字符且两侧须为字母、至多一个标点且必须位于末尾)。它的解法先把句子按空格切分,再对每个 token 调用 check 函数计数——其主流程与本题的「分词 + 计数」框架一脉相承,只是把「计数条件」从「非空」升级为「合法」;
  • Index/模拟.md:仓库按 Tag 维护了模拟类题目的总表,其中收录了 434 题及其题解链接,方便按标签系统性刷题。

可以看出:「空格分词 + 逐段处理」是这类题目的通用骨架。本题考的是最简单的「段数统计」,58 题考的是「定位特定段」,2047 题考的是「逐段合法性判定」——掌握 434 的一次遍历模拟写法,就等于掌握了这套框架的入口。

七、总结

LeetCode 434「字符串中的单词数」是一道极具代表性的「模拟」类入门题:题目定义干净(单词 = 连续非空格字符)、解法直白(一次遍历,空格跳过、非空格段计数)、复杂度优秀(O(n) 时间 / O(1) 空间)。原题解用一个 if-while 组合完成了全部逻辑,其中 i++ >= 0 的短路技巧值得反复品味。

复习要点:

  1. 吃透定义:本题的「单词」只看空格一个分隔符,标点、数字都算单词内容;
  2. 掌握写法:能独立写出并讲清楚「跳过空格 + 消费整段 + 计数」的模拟循环;
  3. 理解复杂度:单次线性扫描、常数额外空间,是优于 split 方案的关键;
  4. 建立框架:将 434、58、2047 三题串联,形成「分词 → 定位 / 计数 / 校验」的完整解题思维。

完整的题解原文见仓库文件 LeetCode/431-440/434. 字符串中的单词数(简单).md,这是「刷穿 LeetCode」系列的第 No.434 篇(该系列自 2021/01/01 启动,目标是将当时全部不带锁的题目逐一刷完),仓库内所有题解均遵循「题目描述 → 解法 → 复杂度 → 系列说明」的统一结构,并尽可能给出最简洁的代码。

  • 教程
  • 文档

【免费下载链接】LogicStack-LeetCode

公众号「宫水三叶的刷题日记」刷穿 LeetCode 系列文章源码

项目地址: https://gitcode.com/gh_mirrors/lo/LogicStack-LeetCode
点击查看 免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值