第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 动态记录:不只是长度,还要位置
很多初学者只记录最长单词的长度,最后再回头去找这个单词。这其实多了一次遍历。我个人的做法是:同时记录最长单词的起始位置和长度。这样遍历结束后,直接通过指针偏移就能拿到结果。
你看上面的代码,我用了 maxStart 和 maxLen 两个变量。这就是“动态记录”的精髓——不保存整个单词,只保存它的位置信息。对于大字符串来说,这能省不少内存。
避坑指南:我曾经犯过一个错误——在单词结束时直接更新 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 图来总结本章的核心逻辑。这张图展示了从输入到输出的完整流程,以及各个关键决策点。
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 char 或 EOF。如果直接传入 char,在某些平台上(如 char 默认 signed 的 ARM),可能会因为符号扩展导致未定义行为。这个坑我踩过,印象特别深。
15.9 总结
好了,关于字符串中查找最长单词,咱们就聊这么多。核心就是:一次遍历,动态记录。记住要同时记录位置和长度,别忘了处理边界情况,还有字符判断函数的选择要谨慎。
这个算法虽然简单,但它是很多复杂字符串处理问题的基础。比如文本分词、关键词提取、日志解析等,底层逻辑都差不多。你把这个吃透了,后面遇到类似问题就能举一反三。
最后送大家一句话:简单的问题,往往藏着最深的坑。把基础打牢,才能走得更远。
公众号:蓝海资料掘金营,微信deep3321