第15章:字符串中查找最长单词——遍历与动态记录

各位同学,咱们今天聊一个非常经典的问题:在字符串中查找最长的单词

这个问题看起来简单,对吧?不就是遍历一遍,记录下最长的那个嘛。但说实话,我当年刚入行时,在这个问题上栽过跟头。有一次我写一个日志解析工具,需要从一行文本中提取最长的标识符。我自认为写得没问题,结果上线后,遇到空字符串就崩了,遇到连续空格也崩了……嗯,从那以后,我再也不敢小看这个“简单”的问题了。

15.1 问题定义与核心思路

先明确一下我们要做什么:

  • 输入:一个字符串(可能包含空格、标点、换行等)
  • 输出:该字符串中最长的单词(如果有多个,通常返回第一个)
  • 单词定义:连续的非空白字符序列(或者更严格地,连续字母)

核心思路其实就四个字:遍历 + 记录

你想想看,我们只需要从头到尾扫描字符串,遇到单词就开始计数,遇到分隔符就结束当前单词。在这个过程中,动态地记录下当前遇到的最长单词及其长度。说白了,就是一边走一边比,谁长就记谁。

核心思想:一次遍历,两个指针(或索引),一个记录当前单词起点,一个记录当前扫描位置。同时维护最长单词的起点和长度。

15.2 基础实现:逐字符遍历法

我个人习惯用最直接的方式——逐字符遍历。咱们先看代码:

#include <stdio.h>
#include <string.h>
#include <ctype.h>

void findLongestWord(const char *str, char *result) {
    if (str == NULL || result == NULL) return;

    int len = strlen(str);
    int maxLen = 0;
    int maxStart = 0;
    int curStart = -1;  // 当前单词起始位置,-1表示不在单词中
    int curLen = 0;

    for (int i = 0; i <= len; i++) {
        // 判断当前字符是否为字母(或单词字符)
        if (isalpha(str[i])) {
            if (curStart == -1) {
                curStart = i;  // 新单词开始
                curLen = 1;
            } else {
                curLen++;      // 继续当前单词
            }
        } else {
            // 遇到非字母字符,单词结束
            if (curStart != -1) {
                if (curLen > maxLen) {
                    maxLen = curLen;
                    maxStart = curStart;
                }
                curStart = -1;  // 重置
                curLen = 0;
            }
        }
    }

    // 复制最长单词到result
    if (maxLen > 0) {
        strncpy(result, str + maxStart, maxLen);
        result[maxLen] = '\0';
    } else {
        result[0] = '\0';
    }
}

int main() {
    char text[] = "I love programming in C language!";
    char longest[100];
    findLongestWord(text, longest);
    printf("最长单词: %s\n", longest);  // 输出: programming
    return 0;
}

这段代码里,我用了 isalpha() 来判断字符。为什么不用 isspace()?因为我想把标点符号也当作分隔符。你想想看,如果字符串是 "hello,world",用空格做分隔符的话,"hello," 会被当成一个单词,这显然不对。

小技巧:判断单词边界时,用 isalpha()isalnum() 比用 isspace() 更安全。这样可以自动过滤掉标点符号。

15.3 进阶:处理多种分隔符

实际项目中,字符串里的分隔符可不止空格和标点。我记得有一次处理用户输入的搜索关键词,里面混了制表符、换行符,甚至还有不间断空格(\u00A0)。那叫一个头疼。

所以,我建议定义一个“单词字符”的判断函数:

int isWordChar(char c) {
    return isalnum(c) || c == '_' || c == '-';
    // 根据需求,可以加入更多合法字符
}

然后在遍历时,用这个函数代替 isalpha()。这样灵活性就高多了。

15.4 动态记录:不只是长度,还要位置

很多初学者只记录最长单词的长度,最后再回头去找这个单词。这其实多了一次遍历。我个人的做法是:同时记录最长单词的起始位置和长度。这样遍历结束后,直接通过指针偏移就能拿到结果。

你看上面的代码,我用了 maxStartmaxLen 两个变量。这就是“动态记录”的精髓——不保存整个单词,只保存它的位置信息。对于大字符串来说,这能省不少内存。

避坑指南:我曾经犯过一个错误——在单词结束时直接更新 maxLen,但忘了更新 maxStart。结果最长单词的位置永远是上一次的。记住:长度和位置必须同步更新

15.5 处理边界情况

写代码最怕什么?边界情况。我列几个常见的坑:

  • 空字符串:直接返回空,不要崩溃。
  • 全是分隔符:没有单词,返回空。
  • 单词在开头或结尾:遍历到字符串末尾的 '\0' 时,要能正确结束最后一个单词。
  • 多个单词等长:通常返回第一个,但需求可能不同。
  • 中文字符:如果字符串包含中文,isalpha() 会返回 false,需要特殊处理。

关于中文字符,我多说一句。在 C 语言中,中文字符用多字节编码(如 UTF-8),每个汉字占 2~3 个字节。直接用 isalpha() 判断会出问题。我建议要么用宽字符(wchar_t),要么自己写一个 UTF-8 字符判断函数。不过那是另一个话题了,咱们今天先聚焦在纯英文场景。

15.6 性能分析

这个算法的时间复杂度是 O(n),n 是字符串长度。空间复杂度是 O(1),只用了几个变量。你想想看,无论字符串多长,我们只需要一次遍历,而且不额外分配内存。这在嵌入式系统或资源受限的环境中特别有用。

指标
时间复杂度 O(n)
空间复杂度 O(1)
适用场景 任意长度字符串
稳定性 稳定(不改变输入)

15.7 知识体系图

下面我用一张 SVG 图来总结本章的核心逻辑。这张图展示了从输入到输出的完整流程,以及各个关键决策点。

字符串中查找最长单词——核心逻辑 输入字符串 初始化变量 maxLen=0, curStart=-1, curLen=0 遍历每个字符(i从0到len) 当前字符: str[i] 是单词字符? 更新当前单词 curLen++ 或 curStart=i 单词结束 比较并更新最长单词

15.8 完整代码示例(含边界处理)

最后,我给出一个更健壮的版本。这个版本我加了空指针检查、空字符串处理,以及用 isalnum() 替代 isalpha(),让数字也能被包含在单词中。

#include <stdio.h>
#include <string.h>
#include <ctype.h>

/**
 * 查找字符串中最长的单词
 * @param str  输入字符串
 * @param result 输出缓冲区,存放最长单词
 * @param resultSize 输出缓冲区大小
 * @return 最长单词的长度,如果没有单词返回0
 */
int findLongestWordSafe(const char *str, char *result, size_t resultSize) {
    if (str == NULL || result == NULL || resultSize == 0) {
        return 0;
    }

    result[0] = '\0';  // 初始化为空字符串

    size_t len = strlen(str);
    size_t maxLen = 0;
    size_t maxStart = 0;
    size_t curStart = 0;
    size_t curLen = 0;
    int inWord = 0;  // 标志位:是否在单词中

    for (size_t i = 0; i <= len; i++) {
        if (isalnum((unsigned char)str[i])) {
            if (!inWord) {
                inWord = 1;
                curStart = i;
                curLen = 1;
            } else {
                curLen++;
            }
        } else {
            if (inWord) {
                // 单词结束,检查是否是最长的
                if (curLen > maxLen) {
                    maxLen = curLen;
                    maxStart = curStart;
                }
                inWord = 0;
                curLen = 0;
            }
        }
    }

    // 复制结果
    if (maxLen > 0) {
        size_t copyLen = (maxLen < resultSize - 1) ? maxLen : (resultSize - 1);
        strncpy(result, str + maxStart, copyLen);
        result[copyLen] = '\0';
    }

    return (int)maxLen;
}

int main() {
    // 测试用例
    const char *tests[] = {
        "Hello world!",
        "   ",           // 全是空格
        "",              // 空字符串
        "A",
        "one two three four five six seven eight nine ten",
        "C programming language is powerful",
        NULL
    };

    char result[100];
    for (int i = 0; tests[i] != NULL; i++) {
        int len = findLongestWordSafe(tests[i], result, sizeof(result));
        printf("输入: \"%s\"\n", tests[i]);
        printf("最长单词: \"%s\" (长度: %d)\n\n", result, len);
    }

    return 0;
}

注意:上面的代码中,我用了 (unsigned char)str[i] 做类型转换。这是因为 isalnum() 等函数要求参数是 unsigned charEOF。如果直接传入 char,在某些平台上(如 char 默认 signed 的 ARM),可能会因为符号扩展导致未定义行为。这个坑我踩过,印象特别深。

15.9 总结

好了,关于字符串中查找最长单词,咱们就聊这么多。核心就是:一次遍历,动态记录。记住要同时记录位置和长度,别忘了处理边界情况,还有字符判断函数的选择要谨慎。

这个算法虽然简单,但它是很多复杂字符串处理问题的基础。比如文本分词、关键词提取、日志解析等,底层逻辑都差不多。你把这个吃透了,后面遇到类似问题就能举一反三。

最后送大家一句话:简单的问题,往往藏着最深的坑。把基础打牢,才能走得更远。


公众号:蓝海资料掘金营,微信deep3321