云计算百科
云计算领域专业知识百科平台

JAVA:实现指定字符串中的单词的个数算法(附带源码)

1. 项目背景详细介绍

在文本处理、信息检索、自然语言处理、搜索引擎统计、日志分析等诸多领域,统计指定字符串中“单词”的个数是一个非常基础但又容易产生歧义的任务。看似简单的“单词计数”(word count)在不同语言、不同场景(如带标点的句子、缩写、连字符、撇号、中文/日文无空格语言、混合语言)下会有不同的定义与实现细节。教学时,统计单词个数非常适合作为讲解正则表达式、字符串分割、有限状态机(FSM)、Java 文本边界工具(BreakIterator)、以及性能/内存权衡的案例。

本项目面向课堂与工程两方面:既要有简单直接、便于理解的实现,也要提供更稳健、国际化的解决方案,并讨论边界情况与性能考量。目标是让学生理解“什么是单词”的多种定义,以及如何在 Java(兼容 Java 8)中实现相应算法。


2. 项目需求详细介绍

  • 提供多种 Java 实现方式用于统计指定字符串中的单词数,至少包含:

    • 简单的按空白分割实现(适合英文简单文本);

    • 基于正则的实现(可自定义规则,支持常见缩写/撇号/连字符);

    • 基于 BreakIterator 的区域/语言感知实现(支持国际化);

    • 基于有限状态机(FSM)的鲁棒实现(逐字符判断,低依赖、可扩展);

    • 演示对中文/日文等“无空格”语言的说明与建议(含简单字符计数策略与第三方分词库建议)。

  • 所有代码需兼容 Java 8+,注释详细,便于课堂讲解。

  • 所有实现放在单个代码块内,不同“文件”用注释区分(例如 // File: …)。

  • 提供主方法示例(含多种输入案例),输出每种方法的计数结果以便对比。

  • 在文档中详细说明每种实现的优缺点、时间/空间复杂度、适用场景与注意事项。

  • 文章中文详尽、面向教学:解释为何选择此实现、常见陷阱与推荐做法。


  • 3. 相关技术详细介绍

    • 字符分类:Character.isLetter、Character.isLetterOrDigit、Unicode 类别 \\p{L} 等,用于判断字符是否属于“词内字符”;

    • 正则表达式:String.split() 和 Pattern/Matcher,常见正则如 \\b、\\p{L}、[A-Za-z0-9'-] 等;

    • BreakIterator:java.text.BreakIterator.getWordInstance(Locale),用于基于语言的词边界检测(比简单正则更贴合本地化规则);

    • Scanner:java.util.Scanner 可作为快速的分词工具,但需要设置合适的分隔符;

    • 有限状态机(FSM):按字符逐步判断状态(IN_WORD / OUT_WORD),精确控制何为“单词开始/结束”;

    • 第三方分词:对于中文/日文,需使用分词库(如jieba、HanLP)或 NLP 工具包才能做到真正的“单词/词语”分割;

    • 性能/内存:字符串长度极大时,避免创建大量中间字符串(如 split 会产生数组),应考虑流式/逐字符处理或使用 Scanner/BreakIterator 增量处理。


    4. 实现思路详细介绍

    我们依次实现以下方法并在 main 中演示:

  • countBySimpleSplit(String s):trim() 后使用 s.split("\\\\s+") 得到 token 数,适合以空白分割的简单场景(英文短文本)。优点:实现最简单;缺点:不能处理附带标点的 token 较准。

  • countByRegex(String s):使用正则 \\b[\\p{L}\\p{Nd}]+(?:['-][\\p{L}\\p{Nd}]+)*\\b(支持字母/数字,允许内部连字符和撇号),通过 Matcher 逐个匹配并计数。优点:可通过正则自定义识别规则;缺点:复杂正则较难读懂且对特殊情况有限。

  • countByBreakIterator(String s, Locale locale):使用 BreakIterator.getWordInstance(locale) 遍历候选词边界,计数时只把包含字母/数字字符的边界作为词。优点:国际化支持更好;缺点:与语言规则一致性依赖 Java 的实现,某些语言(中文)仍需分词库。

  • countByFSM(String s):实现基于状态机的逐字符扫描:当遇到字母/数字则进入 IN_WORD 状态,遇到非词字符则从 IN_WORD 切换到 OUT_WORD 并计数。优点:内存友好、性能可控、实现逻辑清晰;缺点:需定义“字母/数字”的精确判定(Unicode 较复杂)。

  • countChineseByChar(String s):对于中文和日文等无空格语言,提供一个可选的“把每个汉字/汉字序列视为词”的简易策略(非真正的分词,仅教学用途),并建议使用专业分词库做精确分词。

  • countByScanner(String s):演示 Scanner 的简单用法(useDelimiter("\\\\s+")),但要注意其分隔符与 token 清洗。

  • 主方法会对常见示例进行对比:包含简单英文句子(带标点)、包含缩写与撇号(don't, O'Reilly)、连字符(state-of-the-art)、混合中英文、以及中文句子,展示不同方法下计数差异并讨论原因。


    5. 完整实现代码

    // File: WordCountUtils.java
    // 说明:多种实现统计字符串中单词(word)个数的工具类,兼容 Java 8+。
    // 方法包括:简单 split、正则匹配、BreakIterator(国际化)、FSM(状态机)、Scanner 示例、中文简易计数。
    // 代码以教学为主,注释详尽,便于课堂讲解。

    import java.text.BreakIterator;
    import java.util.Locale;
    import java.util.Scanner;
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;

    public class WordCountUtils {

    /**
    * 方法1:简单按空白符分割(最直观)
    * 说明:
    * – 使用 trim() 去掉前后空白,使用 \\s+ 作为分隔符。
    * – 返回 split 后的 token 数(忽略空串)。
    * – 适用于以空格分隔的英文/拉丁字母文本,但不能处理带标点的复杂情况。
    * 时间复杂度:O(n),空间复杂度:O(k)(生成 token 数组)
    */
    public static int countBySimpleSplit(String s) {
    if (s == null) return 0;
    String trimmed = s.trim();
    if (trimmed.isEmpty()) return 0;
    // 按任意空白字符序列分割(空格、制表符、换行等)
    String[] tokens = trimmed.split("\\\\s+");
    return tokens.length;
    }

    /**
    * 方法2:基于正则的更精细匹配(支持字母、数字、撇号、连字符)
    * 说明:
    * – 使用 Pattern/Matcher 逐个匹配“单词”的边界,而不是全局 split。
    * – 下面的正则示例:\\b[\\p{L}\\p{Nd}]+(?:['-][\\p{L}\\p{Nd}]+)*\\b
    * 解释:
    * \\p{L} 代表 Unicode 字母类别
    * \\p{Nd} 代表 Unicode 十进制数字
    * (?:['-][…]+)* 允许内部出现连字符或撇号(例如 don't, state-of-the-art)
    * – 该规则可根据需要调整(例如允许下划线、百分号等)。
    * 时间复杂度:O(n)(正则匹配),空间复杂度:O(1)(不保存所有 token)
    */
    public static int countByRegex(String s) {
    if (s == null) return 0;
    // 以 Unicode 类别为基础的正则,支持多语言字母和数字
    String regex = "\\\\b[\\\\p{L}\\\\p{Nd}]+(?:['-][\\\\p{L}\\\\p{Nd}]+)*\\\\b";
    Pattern p = Pattern.compile(regex);
    Matcher m = p.matcher(s);
    int count = 0;
    while (m.find()) {
    count++;
    }
    return count;
    }

    /**
    * 方法3:使用 BreakIterator(基于 Locale 的词边界检测)
    * 说明:
    * – BreakIterator.getWordInstance(locale) 提供语言敏感的“单词”边界检测。
    * – 迭代所有边界区间 [start, end),并检查该区间内是否包含字母或数字字符(排除空格和标点)。
    * – 对于英文等按空格分词的语言,效果通常很好;对于中文需要更复杂的分词器。
    * 时间复杂度:O(n),空间复杂度:O(1)
    */
    public static int countByBreakIterator(String s, Locale locale) {
    if (s == null || s.isEmpty()) return 0;
    BreakIterator boundary = BreakIterator.getWordInstance(locale);
    boundary.setText(s);
    int start = boundary.first();
    int count = 0;
    for (int end = boundary.next(); end != BreakIterator.DONE; start = end, end = boundary.next()) {
    // 子串 [start, end)
    String word = s.substring(start, end);
    // 只把含有字母或数字的片段视为单词(过滤标点和空白)
    boolean hasLetterOrDigit = false;
    for (int i = 0; i < word.length(); i++) {
    char ch = word.charAt(i);
    if (Character.isLetterOrDigit(ch)) {
    hasLetterOrDigit = true;
    break;
    }
    }
    if (hasLetterOrDigit) count++;
    }
    return count;
    }

    /**
    * 方法4:有限状态机(FSM)逐字符扫描(鲁棒、内存友好)
    * 说明:
    * – 定义两个状态:IN_WORD(当前在单词内部),OUT_WORD(当前不在单词内部)。
    * – 遍历字符串的每个字符:如果字符被判定为“词字符”(默认使用 isLetterOrDigit),则进入/保持 IN_WORD;
    * 否则(空白、标点),若之前处于 IN_WORD,则计数并切换到 OUT_WORD。
    * – 结束时若处于 IN_WORD 则再计数一次。
    * – 优点:不产生额外的中间字符串或数组,适合长文本流式处理。
    * – 可通过替换 isLetterOrDigit 判定逻辑来自定义何为“词字符”。
    */
    public static int countByFSM(String s) {
    if (s == null || s.isEmpty()) return 0;
    final int OUT_WORD = 0;
    final int IN_WORD = 1;
    int state = OUT_WORD;
    int count = 0;
    for (int i = 0; i < s.length(); i++) {
    char ch = s.charAt(i);
    boolean isWordChar = Character.isLetterOrDigit(ch); // 可替换为更复杂判断
    if (isWordChar) {
    if (state == OUT_WORD) {
    state = IN_WORD; // 单词开始
    }
    // 若已经在 IN_WORD,继续
    } else {
    if (state == IN_WORD) {
    count++; // 单词结束
    state = OUT_WORD;
    }
    }
    }
    if (state == IN_WORD) count++; // 字符串末尾的单词
    return count;
    }

    /**
    * 方法5:Scanner 演示(简捷但需注意分隔符和清洗)
    * 说明:
    * – Scanner 可以通过 useDelimiter 设置分隔符,这里示例使用默认的分隔符(空白)。
    * – 对于含标点的 token,需进一步清洗(例如去掉两端标点)以得到更精确的单词。
    * – Scanner 在构造大量对象或高并发场景可能不及简单 FSM 效率高。
    */
    public static int countByScanner(String s) {
    if (s == null) return 0;
    int count = 0;
    Scanner scanner = new Scanner(s);
    // 默认分隔符为空白,模拟简单的空白分割计数
    while (scanner.hasNext()) {
    String token = scanner.next();
    // 可做进一步清洗:去掉两端标点
    token = token.replaceAll("^[\\\\p{Punct}]+|[\\\\p{Punct}]+$", "");
    if (!token.isEmpty()) count++;
    }
    scanner.close();
    return count;
    }

    /**
    * 方法6:对中文/日文等无空格语言的简易策略(教学演示)
    * 说明:
    * – 对于中文/日文,单纯按空格或 \\p{L} 并不足以得到词级分割。最准确的方法是使用分词器(如 jieba, HanLP 等)。
    * – 这里提供一个非常简化的策略:把连续的汉字序列视为一个“词”,使用 Unicode Block 判断是否为 CJK。
    * – 该方法仅为教学用途,不能替代真正的分词器。
    */
    public static int countChineseByChar(String s) {
    if (s == null || s.isEmpty()) return 0;
    int count = 0;
    boolean inHan = false;
    for (int i = 0; i < s.length(); i++) {
    char ch = s.charAt(i);
    boolean isHan = Character.UnicodeBlock.of(ch) == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS
    || Character.UnicodeBlock.of(ch) == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A
    || Character.UnicodeBlock.of(ch) == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS
    || Character.UnicodeBlock.of(ch) == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION;
    if (isHan) {
    if (!inHan) {
    inHan = true;
    count++;
    }
    } else {
    inHan = false;
    }
    }
    return count;
    }
    }

    // File: Main.java
    // 演示各种方法的效果与对比,含多种测试用例,适合课堂运行和现场讲解。

    import java.util.Locale;
    import java.nio.charset.StandardCharsets;

    public class Main {
    public static void main(String[] args) {
    String s1 = "Hello, world! This is a test.";
    String s2 = "Don't stop-believing: it's state-of-the-art.";
    String s3 = "O'Reilly's book on C++: 100% practical.";
    String s4 = "混合 English 和 中文 的句子。测试分词。";
    String s5 = "这是中文没有空格的句子,用于演示简易汉字统计。";

    System.out.println("示例文本及各种方法的计数结果:\\n");

    demo(s1);
    demo(s2);
    demo(s3);
    demo(s4);
    demo(s5);
    }

    private static void demo(String s) {
    System.out.println("原文: " + s);
    System.out.println("1) SimpleSplit: " + WordCountUtils.countBySimpleSplit(s));
    System.out.println("2) Regex: " + WordCountUtils.countByRegex(s));
    System.out.println("3) BreakIterator (Locale.ENGLISH): " + WordCountUtils.countByBreakIterator(s, Locale.ENGLISH));
    System.out.println("4) FSM: " + WordCountUtils.countByFSM(s));
    System.out.println("5) Scanner: " + WordCountUtils.countByScanner(s));
    System.out.println("6) Chinese simple (汉字序列计数): " + WordCountUtils.countChineseByChar(s));
    System.out.println("—-\\n");
    }
    }

    6. 代码详细解读

    (下列说明仅描述每个方法的功能与适用场景,不重复代码实现细节。)

    • countBySimpleSplit(String s):对字符串 s 做 trim() 后以空白字符串(\\\\s+)分割并返回 token 数。这是最直观的实现,适合纯英文且以空格分隔的文本,但对标点、撇号、连字符没有特殊处理。

    • countByRegex(String s):使用基于 Unicode 类别的正则表达式匹配单词(支持字母、数字、内部撇号或连字符),用 Matcher.find() 逐个计数。适用于需要灵活控制“什么算作单词”的场景(例如允许 don't、state-of-the-art)的英文文本。

    • countByBreakIterator(String s, Locale locale):利用 BreakIterator.getWordInstance(locale) 获取语言敏感的词边界,遍历边界并筛选含字母或数字的区间作为单词。适合国际化文本,优点是减少对简单正则的手写判断,依赖 JVM 提供的词边界规则。

    • countByFSM(String s):使用有限状态机(IN_WORD / OUT_WORD)对字符串逐字符扫描,通过 Character.isLetterOrDigit 判断词字符,从而以流式、内存友好的方式统计单词数。适合长文本或流式处理场景,便于定制“词字符”判定策略。

    • countByScanner(String s):演示 Scanner 的基本使用(默认按空白分隔),并演示对 token 做两端标点清洗后计数。适合快速实现和简单脚本,但在性能/灵活性上不如 FSM 或 BreakIterator。

    • countChineseByChar(String s):对中文/日文等无空格语言提供的极简策略:把连续的汉字序列视为一个词(通过 UnicodeBlock 判断 CJK 字符),仅用于教学演示。真实场景建议使用专门的分词库(如 Jieba、HanLP、IKAnalyzer 等)来得到词级划分。


    7. 项目详细总结

    本文从多个角度实现并讲解了“统计指定字符串中单词个数”的问题。关键要点:

  • “单词”的定义决定算法复杂度:在不同语言与业务场景下“单词”含义不同——英文中通常以空白/标点分割,而中文需要分词库,专业术语或缩写(如 e-mail、O'Reilly)会导致简单方法失效。

  • 多种实现各有优缺点:

    • split:最简单,但对标点/缩写/连字符鲁棒性差;

    • regex:灵活可控,但正则复杂且维护成本高;

    • BreakIterator:本地化表现更好,推荐用于多语言环境;

    • FSM:内存友好、性能稳定,适合流式处理和大文本;

    • 中文等无空格语言需要分词器支持。

  • 性能考虑:对短文本三种方法差别不大;对超长文本或高吞吐场景,应优先使用 FSM 或表驱动/流式方法,避免 split 产生大量中间对象。

  • 工程建议:默认情况可采用 BreakIterator(国际化)或 FSM(高性能通用);对中文/日文或需要语义级分词的场景,接入成熟分词库并做停用词/词形还原等预处理。


  • 8. 项目常见问题及解答

    Q1:为什么 split("\\\\s+") 有时会多算或少算单词?
    A1:因为它只按空白分割,不会剔除标点或识别缩写与连字符,导致 "hello," 会作为 "hello,"(含逗号)计数为一个 token,但如果你要求剔除标点就需额外清洗或用正则匹配。

    Q2:BreakIterator 能否完美处理多语言?
    A2:BreakIterator 提供了语言敏感的边界检测,对于英文、法文等基于空格的语言表现良好,但对中文/日文等无空格的语言并不执行语义分词(仅能检测字符/标点),仍建议使用分词器。

    Q3:FSM 是否能处理 Unicode 较复杂的情况?
    A3:可以,但 Character.isLetterOrDigit 是较保守的判断。具体业务可扩展判定规则(例如把撇号、连字符视为词内字符),也可根据 UnicodeBlock 进一步细分。

    Q4:如何统计带 HTML 标签或实体的文本中的单词?
    A4:先进行预处理:剥离 HTML 标签(例如使用 Jsoup),将实体转为文本,再对纯文本做单词统计。

    Q5:如何处理缩写、数字、带百分号或特殊符号的 token?
    A5:这属于业务规则,需要定义哪些符号允许出现在单词内部(如 3rd, 50%, e-mail),并据此调整正则或 FSM 判定逻辑。


    9. 扩展方向与性能优化

  • 分词库集成(中文/日文/韩文):集成 Jieba/HanLP/IKAnalyzer 等能给出词级分割并支持停用词、词性标注与命名实体识别。

  • 流式/并行处理:对大文件或日志流分块并发处理,注意分块边界处的单词断裂问题(需 carry-over 片段)。

  • 更精细的 Unicode 支持:考虑复合字符(combining marks)、emoji(表情符号)以及右到左文字(RTL)处理。

  • 语义化分词:在需要语义分析场景下,引入词形还原(lemmatization)与去除停用词以获得更有意义的“单词计数”。

  • 内存优化:避免 split 等创建大量中间对象,改用 FSM/BreakIterator 或 streaming API(逐块读取并处理)以降低 GC 压力。

  • 基准测试:使用 JMH 对多种实现进行基准测试(不同输入长度、不同语言),找出在目标场景下最优的实现。

  • 赞(0)
    未经允许不得转载:网硕互联帮助中心 » JAVA:实现指定字符串中的单词的个数算法(附带源码)
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!