ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PAT乙级1009说反话:C语言字符串读取与逆序输出全解析

PAT乙级1009说反话:C语言字符串读取与逆序输出全解析 1. 先说题目PAT乙级1009到底在考什么1.1 一句话读题把单词顺序整个反过来PAT乙级1009题目叫说反话是绝大多数人刷PAT题库时最早碰上的字符串题之一。题目描述很朴素给定一句英文单词之间用空格分开要求你把这句英语中的所有单词顺序颠倒后输出。比如输入Hello World Here I Come输出就得是Come I Here World Hello。我第一次看这道题时觉得这有什么难的——定义一个数组把句子存下来遇到空格就分词最后倒着输出一遍完事。结果真正动手写才发现题目埋的坑根本不在逆序这一步而在怎么把一行带空格的句子完整读进来。C语言里最常用的scanf(%s, ...)一到空格就停了你原封不动地用上去读进来的永远是第一个单词。这个坎不迈过去后续写再多都是白搭。所以这道题虽然代码量很少但它非常适合用来检验你对C语言字符串输入输出到底理解到什么程度。后面我看到的很多PAT乙级题比如字符统计、单词查找、进制转换本质上都和这题共用同一套基本功。可以说1009是乙级字符串题的敲门砖。1.2 真正的考点是怎么把带空格的句子读进来scanf(%s, str)的行为是跳过开头的空白字符然后一直读到空白字符为止。注意这个空白字符是个广义概念空格、制表符、换行符都算。所以scanf(%s, ...)天生就是按单词读取的而不是按一行读取。很多人一上来就scanf(%s, s)输入Hello World Here I Come程序拿到的只有Hello后面 World Here I Come 全都留在缓冲区里。你说我把printf(%s)改成循环多调用几次那读进来的顺序还是从左到右的单词序列确实能拿到所有词只是需要循环到读到EOF为止这就是后面要讲的方案一。很多人卡住是因为没意识到%s的语义以为它可以读一整行。如果确实想读一行C语言里有gets、fgets、scanf(%[^\n])这些读取整行的手段。但它们各自的边界行为又不一样比如gets在新标准里直接移除了fgets会把换行符\n也读进数组scanf(%[^\n])需要配合清空缓冲区这些细节才是题目真正想考察的。1.3 逆序逻辑本身没有难度把单词分隔好之后剩下的倒序输出就是一个数组遍历问题。假如你分割出了count个单词存进二维数组words[0]到words[count-1]只需要这样for (int i count - 1; i 0; i--) { printf(%s, words[i]); if (i ! 0) { printf( ); } }唯一要控制的是空格最后一个单词后面不要跟空格每个单词之间刚好一个空格。题目输出要求里明确讲了句子末尾不要有多余空格这个用if (i ! 0)就能搞定。核心结论这道题表面是说反话本质是你会不会安全地读入并切分一行字符串。搞明白这个你就知道为什么网上搜到的题解会有那么多写法——它们不是在炫技而是在用不同方式解决同一个输入难题。2. C语言的三种输入方案与取舍刷PAT乙级字符串题的输入方案大同小异吃透这三招后面遇到类似题都能直接套。2.1 用scanf(%s)循环读单词最省心的方案既然%s本身就按空白分割单词那就别跟它对着干直接反复调用把整段输入里的所有单词全部读进来。完整代码#include stdio.h int main() { char words[100][100]; int count 0; while (scanf(%s, words[count]) ! EOF) { count; } for (int i count - 1; i 0; i--) { if (i ! count - 1) { printf( ); } printf(%s, words[i]); } printf(\n); return 0; }这个方案最大的优点是你完全不用关心输入里到底有几个空格、单词之间是不是严格一个空格甚至连句子中间换行都不怕。PAT在线测评时评测程序会向程序的标准输入发送完整测试数据并在结束时关闭输入流程序读不到更多内容时scanf会返回EOF循环自然终止。我在实际刷题时最推荐先写这个版本。它的代码最少逻辑最简单几乎不会因为输入格式的细枝末节出错。唯一的注意点是二维数组的大小题目说一行总长度不超过80所以100个单词、每个单词最多100字符的分配是绝对够用的。2.2 用gets读一行再按空格拆最直观但警惕编译环境另一种思路是先读一整行再用分割函数拆单词。老版教科书里常用gets#include stdio.h #include string.h int main() { char str[100]; char words[100][100]; int count 0; gets(str); char *token strtok(str, ); while (token ! NULL) { strcpy(words[count], token); count; token strtok(NULL, ); } for (int i count - 1; i 0; i--) { if (i ! count - 1) { printf( ); } printf(%s, words[i]); } printf(\n); return 0; }这段代码逻辑很清晰gets负责把整行包括空格都装进strstrtok按空格把句子切成一个一个单词存入二维数组再逆序输出。但注意gets在C11标准中已经被正式移除因为它没有办法指定读取长度输入稍长就会缓冲区溢出历史上出过不少安全问题。现在很多新版本编译环境你编译这段代码会直接报gets undeclared。PAT以前的老题解大量使用gets新同学直接复制过来会发现编译都过不了这就是踩了环境变迁的坑。2.3 用fgets作为gets的替代新版编译器下的正确姿势fgets是标准C函数是按行读取的正统做法。它的签名是fgets(char *s, int size, FILE *stream)需要自己指定缓冲区大小有效避免溢出。用fgets替代gets的完整写法#include stdio.h #include string.h int main() { char line[100]; char words[100][100]; int count 0; if (fgets(line, sizeof(line), stdin) ! NULL) { char *token strtok(line, \n); while (token ! NULL) { strcpy(words[count], token); count; token strtok(NULL, \n); } } for (int i count - 1; i 0; i--) { if (i ! count - 1) { printf( ); } printf(%s, words[i]); } printf(\n); return 0; }这里有个非常容易翻车的细节fgets读入一行后换行符\n也会被存进数组。如果你用strtok(line, )最后一个单词后面还挂着\n输出时会导致格式异常。解决办法就是我在代码里写的把分隔符写成 \n让strtok把换行符也当作分隔符处理或者在切词之前手动把末尾的\n替换成\0。这道题因为最终要逆序输出所有单词 \n这种写法更干净。2.4 三种方案对比与我的推荐方案读取方式能否处理多空格是否受编译器影响代码难度推荐度scanf(%s)循环按空白自动分割能无低最高gets() strtok读整行再分割能新标准下可能编译失败中不推荐fgets() strtok( \n)安全读行再分割能无中高我的建议是在PAT这种单用例、输入内容可控的评测环境里优先用scanf(%s)循环代码短、心智负担小如果你更想训练按行处理的思路就用fgets搭配strtok这也是很多工程场景下的通用写法。无论如何别再用gets了。3. 核心逆序输出边界条件和空格陷阱3.1 从后往前遍历思路就一句话逆序输出很好理解单词存进了数组从最后一个元素往前打印就行。这个思路我已经在上面三份代码里写过不需要额外解释复杂算法。我在这里想强调的是代码能过样例不算本事要保证各种极端输入下都不出问题。比如输入只有一个单词Hello程序应该输出Hello后面不能多空格也不能少换行。上面三份代码在count 1时因为i ! count - 1的条件为假不会打印多余空格输出自然正确。这个简单条件判断能同时处理1个和多个单词的格式需求。3.2 输出格式的细节空格、换行与空输入输出格式是PAT判题最严格的地方。很多人算法想对了却因为多一个空格被判格式错误非常冤枉。关于输出必须记住三条硬性规则单词之间用单个空格分隔不要在中间打两个空格。最后一个单词之后不要输出空格。整个句子结束后输出一个换行让输出规范好看。前面代码里我用的是除了第一个要输出的元素其余元素输出前先打一个空格的写法也就是反过来思考确定要输出某个单词时除非它是倒序后的第一个否则前面都补一个空格。这样从逻辑上杜绝了末尾多空格的问题。至于空输入理论上题目保证至少有一个单词但稳健起见可以在count 0时直接返回避免for循环里用i -1初始化这种不优雅的边界。如果scanf(%s)一个词都没读到说明输入流一开始就是空的此时直接结束程序是最合理的行为。3.3 多行输入的严谨处理PAT 1009的题目描述是测试输入包含一个测试用例在一行内给出总长度不超过80的字符串所以严格来说只有一行。但很多同学拿到的练习平台或者自己扩展的测试数据可能不止一行。如果你用scanf(%s)循环方案读入所有单词后再统一逆序结果会把多行内容合并成一条长句子来逆序。比如输入Hello World I Come实际会读入四个词输出Come I World Hello。如果题目要求每行单独处理、各输出一行逆序结果这个方案就不符合语义了。所以读题很重要看清题目限制是一个用例一行还是多个用例每个一行。如果是后者最好用fgets配合while (fgets(...) ! NULL)逐行处理每读进一行就切分、输出一行。这也是我为什么强调要把三种输入方案都吃透——因为它们在应对不同输入形态时各有优势只背一种代码换个输入格式就容易翻车。4. Python和C解法高级语言怎么作弊4.1 Python的split和切片一行搞定Python写这道题是真正的作弊级体验print( .join(input().split()[::-1]))拆开看input()读一行split()默认按所有空白字符分割并自动清理多余空格[::-1]把单词列表反转 .join(...)用单个空格拼回去。一行代码既解决了输入又解决了分割和逆序。Python这种简洁是建立在语言本身把底层细节都封装好了的基础上。比如split()不仅按空格切还处理了连续多空格、行首行尾空格的情况完全不需要你操心。但恰恰因为太方便很多用Python刷题的同学反而说不清split()到底做了什么一旦遇到split( )和split()结果不一样这类问题就懵。我个人建议用Python参赛刷题没问题但抽空还是要回看一遍C语言怎么处理同样的问题。把底层逻辑补上以后你会更清楚Python替你省了哪些事。4.2 C的istringstream和vectorC的解法比C语言优雅比Python啰嗦但更接近自己掌控过程的感觉#include iostream #include sstream #include vector #include string int main() { std::string line; std::getline(std::cin, line); std::istringstream iss(line); std::vectorstd::string words; std::string word; while (iss word) { words.push_back(word); } for (int i (int)words.size() - 1; i 0; i--) { if (i ! (int)words.size() - 1) { std::cout ; } std::cout words[i]; } std::cout std::endl; return 0; }这里有几个细节值得说。第一getline读入整行必须包含string头文件。第二istringstream配合运算符可以像C语言的scanf一样自动按空白分割单词连续空格也不怕。第三也是很多初学者会踩的坑words.size()返回的是size_t无符号整数如果你写for (int i words.size() - 1; i 0; i--)当words.size()为0时0 - 1会变成一个很大的无符号数循环根本不会按预期停止。我见过不止一个小伙伴因为这个死循环或者越界访问卡了很久。稳妥做法是转成int或者用rbegin/rend迭代器。4.3 语言特性背后的同一个抽象如果你把三种语言都写一遍会发现解题过程殊途同归语言读入整行分割单词存储逆序输出Cfgets/getsstrtok二维字符数组倒序下标Cgetlineistringstreamvectorstring倒序下标或reversePythoninput()split()list[::-1] 切片不管什么语言解题动作始终是读取 → 分割 → 存储 → 逆序 → 拼接。掌握了这层抽象你在语言之间的切换只是换语法不是换思路。这也是我刷题到后期的一个强烈体会算法题的本质是训练思维语言只是表达思维的工具。真正的高手可以一天换三种语言写同一道题因为他琢磨的是输入输出的边界而不是某个函数的拼写。5. 我在刷这道题时踩过的坑和排查过程5.1 坑一scanf(%s)读不到空格句子只剩第一个单词我第一次做这道题犯的错非常典型上来就scanf(%s, s)读入字符串然后兴高采烈地写逆序逻辑最后输入Hello World Here I Come输出只有孤零零一个Hello。百思不得其解还在想是不是自己的逆序代码写反了。后来才反应过来%s遇到空格就停止读取后面的World Here I Come压根没进入过我的程序。用printf(debug: %s, s)一打就现原形了。排查这类问题有一个很实用的方法遇到输出结果和预期不一致时先不要怀疑算法先确认数据到底有没有按你想象的方式进入程序。在输入读取处打一行调试输出看看变量里装了什么比盯着代码干想高效得多。5.2 坑二gets编译报错以及PAT平台编译器版本差异后来我改用gets在本地Dev-C上编译运行一切正常。但提交到OJ平台编译器直接报错提示gets was not declared。这是因为OJ用的编译器可能按照C11标准来gets已经被彻底移除了。这个坑在旧题解里特别常见。老前辈分享的代码大多还停留在gets时代而现在PAT考生用的编译器普遍较新抄老题解的第一步就得先做翻译。遇到这种情况解法就是改用fgets这是C标准库里依然保底的按行读取函数。fgets唯一要记住的是读到的字符串会带换行符记得处理。5.3 坑三fgets带换行符导致的输出异常跟着fgets来的坑是我用strtok(line, )分割时发现最后一个单词总是多出一个换行。比如输入Hello World逆序输出本该是World Hello\n实际却像是World\n Hello\n整个输出乱了。我用调试大法检查最后一个单词把每个字符的ASCII码打印出来才发现Hello后面的换行符\nASCII码10被strtok当成普通字符保留下来了粘在最后一个单词末尾。这种隐藏字符最容易让格式判断出问题光看终端输出很难发现。知道原因就好办了把分隔符改成 \n或者先执行line[strcspn(line, \n)] \0把换行符剔除。我后来写这类字符串题只要用到fgets第一反应就是检查末尾换行这个习惯救了我好多次。5.4 定位这类字符串题WA的一般思路刷题遇到提交答案错误WA不要慌按照下面这套流程排查先拿题目样例测试如果样例挂了直接在关键节点打印中间值定位断点。如果样例通过尝试自己构造边界数据只有1个词、首尾带空格题设不允许但值得试、连续多个空格、超长行。再用16进制或ASCII码方式检查输出内容重点看空格和换行符的位置。最后检查数组边界和自增减类型问题。这套思路熟练之后你会形成肌肉记忆字符串题出错八成是读入方式不对两成是输出格式问题剩下才是真正的逻辑错误。按这个优先级排查效率非常高。6. 从1009出发PAT乙级刷题的正确打开方式6.1 字符串处理题是乙级的题眼刷过PAT乙级的人都会有一个感受这个级别的题目很少考复杂的图论、动态规划高频考点其实集中在字符串处理、进制转换、简单模拟和基础排序。1009正是字符串处理这一类的代表作。把1009吃透之后你会发现它给你打下了一套基础能力安全读取一行、按分隔符切分、处理隐藏换行符、控制输出空格。这些能力在乙级里反复复用比如1037在霍格沃茨找零钱看着是个哈利波特风格的题目骨子里其实是进制转换和数字补位再比如很多字符统计类题目也都绕不开gets/fgets 字符逐个判断的套路。所以我的建议是别急着刷难题先把1009这类基础题用至少两种语言各写一遍把输入输出的基本功夯实。6.2 翁恺老师的课与PAT题库的搭配说到PAT刷题很多初学者都会提到翁恺老师。他的C语言课程讲scanf缓冲区、gets为什么不安全、字符串数组怎么存恰好就是1009需要的知识底色。而且课程配套的实验作业部署在PTA平台上PTA题库把PAT历年真题都收录了学完一个章节就去刷对应的PAT题可以说是很顺滑的学习路线。我自己比较推荐的做法是跟着课程把语法学完然后不急着写大程序直接拿PAT乙级题库练手。每道题先独立写一遍再对照题解查漏补缺。特别是1009这种简单但有小坑的题非常适合用来检验你到底有没有真懂字符串输入。6.3 乙级刷题路线的个人建议如果按我自己刷下来的体感给一个顺序大致是这样先练字符串处理包括读入整行、分词、逆序、字符计数把1009、1021这类题刷明白。再练进制转换和数学模拟比如1022、1037这种巩固对取余、整除、补零的理解。然后接触排序题乙级里的排序不会难重点是学会结构体排序和比较函数。最后才是那些带点递推、模拟味道的综合题这时候前面积累的调试经验就派上用场了。刷题过程中一定不要只复制题解。每道题至少自己写一遍错的题把出错原因记录在注释里隔一周再重做一次。1009这种题我第一次做觉得很简单结果踩了两个坑隔段时间再写又踩了第三个坑。每一次踩坑都是在加深对C语言字符串机制的理解这种经验光看教程是积累不出来的。最后再分享一个小体会很多人觉得这类入门题不值得认真对待但我后来发现恰恰是1009这种简单题最能暴露一个人刷题时粗枝大叶的习惯。把简单题每一次都一次提交通过比反复试探难题边界更能建立手感。对于刚开始刷PAT的朋友认真对待1009就是认真对待后面所有字符串题的第一步。
返回列表