首页 / 资讯中心 / 文章详情

PTA Python词频统计30分题全解析:字符串处理与字典排序实战

PTA Python词频统计30分题全解析:字符串处理与字典排序实战 ★ FEATURED ARTICLE
PTA题库刷到第7章的Python题目很多同学会突然卡住。前面几章大多是三五分、十分左右的小题练的无非是语法点或者某个函数的用法但PTA-Python-第7章-1 词频统计 (30分)这道题一上来就是大分题题面又长动不动就输出一大串结果一不留神就是连续WA好几次。我在实际带学生做这道题的过程中发现它考的根本不只是能不能写出来而是能不能把字符串处理、字典操作、排序规则这三块东西真正串起来。这篇文章就把这道30分题的完整解法、易错点、调试方法全部拆开讲清楚。无论你是刚学Python没多久、正在为这道题头疼的同学还是已经能写但总是差一点ACAccepted的人这篇文章都值得花十分钟看完。我会先讲题目到底在考什么再给出一份可以直接提交的完整代码然后把我自己踩过的坑和学生们常犯的错误一个个列出来。内容比较长但每一步都有原因解释不搞那种复制粘贴能过就行的敷衍路子。1. 题目分析与核心考点1.1 这题到底在考什么一次串起字符串、字典和排序词频统计这四个字放在自然语言处理领域是个很基础的预处理任务但放到PTA的Python题目里它考察的是你对三个核心数据结构的理解是否到位。第一是字符串处理。一段英文文本输入进来里面可能有句号、逗号、括号、换行符甚至可能有数字和连字符你得把这些干扰项全部滤掉只留下真正的单词。很多同学在这个环节就翻车了把句子按空格切分结果hello.和hello被当成两个单词只差一个点词频统计结果天差地别。第二是字典dict的使用。统计频率本质上是键值对的累积过程键是单词值是出现次数。字典的设计初衷就是这个场景但怎么优雅地实现有则加一、无则初始化不同写法之间差距不小。第三是排序。这道题几乎一定会要求按出现次数从高到低排序次数相同则按字典序字母顺序排序这在Python里可以用一行sorted搞定但这一行代码里藏着很多细节比如要不要用reverse、lambda函数怎么定义、元组排序的规则是什么。用错一点结果就是反的。所以这道题30分一点都不冤枉它就像一个综合测验把前面几章学的所有东西汇聚到一个实际场景里。它能帮你检验自己到底是不是真的理解了这些基础内容而不是背了几个API就觉得自己会了。1.2 输入输出格式与边界条件拿满分的第一个前提不同学校可能对这道题的具体输入输出格式做过微调但PTA的常见出题方式是这样的输入一段英文文本可能是多行最后以某个特殊字符结束常见的是单独一行的#。统计时忽略大小写即The和the算同一个词。输出时每行一个单词格式通常是单词:出现次数单词按出现次数降序排列次数相同再按字母升序排列。看起来很简单但边界条件才是真正拉分的地方。第一文本中可能包含连续的标点符号比如hello...world中间三个点算作分隔符也可能包含数字比如Python3按题目要求一般数字不算作单词的一部分需要用正则把它剥离。第二空输入或者只有标点符号的输入。有些同学代码逻辑没有问题但遇到这种情况就报错原因是排序时对一个空字典做了操作或者在循环输出时使用了错误的下标访问方式。这种边界测试用例PTA非常喜欢出因为很多人会忽略。第三大小写混合的情况。Hello、HELLO、hElLo三种写法理论上应该算作同一个单词出现了三次如果你没有做统一大小写的预处理这三个会被统计成三个不同的词输出结果直接错乱。一个聪明的做法是先把整个输入文本统一转换成小写再用正则表达式把连续的英文字母全部提取出来。这样一来大小写问题、标点符号问题、数字混入问题全部被一套操作解决后续代码会轻松很多。关于正则表达式的具体写法后面讲。2. 解题思路拆解从原始文本到排序结果2.1 文本预处理选对工具才能事半功倍拿到整段文本之后第一步不是统计而是清洗。这个清洗过程的目标只有一个把文本变成一列干净的单词列表。最朴素的做法也是很多初学同学的第一反应是使用 text.split() 按空白字符切分。但这样切完之后单词上还挂着标点比如world.、great!还得再做一轮去标点的操作。如果按照空格切分时标点还和单词连在一起你需要再遍历每个单词把非字母字符去掉代码写出来又长又乱还容易漏掉特殊情况。正确且优雅的方案是用正则表达式。Python自带的re模块里re.findall 可以在一行代码里完成找出所有符合条件的子串这件事。针对这道题最合适的模式是 r[a-zA-Z]。它的含义是匹配一个或多个连续的大小写字母匹配到的东西就是单词本体。配合 re.findall 使用它会自动跳过所有非字母字符包括标点、空格、数字、换行符一次返回一个纯单词列表。举个例子text Hello, world! Python3 is great. words re.findall(r[a-zA-Z], text.lower()) print(words)输出结果是 [hello, world, python, is, great]。注意 Python3 里的数字3直接被滤掉了只剩下pythonworld 后的感叹号也没有残留在单词上。这里有一点需要强调的是要把 text.lower() 放在 findall 里面而不是先提取单词再逐个转换小写。虽然两种方式最终效果一样但前者代码更短、语义更清晰。如果你把 lower() 放在 findall 之后你需要写列表推导式来遍历每个单词多了一行不说理解成本也高一些。不过也有同学会问如果是 re.split 呢比如用 re.split(r[^a-zA-Z], text.lower()) 来按非字母字符切分。这个方法可行但它有一个隐蔽的问题如果文本开头或结尾含有非字母字符或者两个非字母字符连续出现split结果里会产生空字符串。虽然可以再过滤空字符串但比起 findall 一步到位还是多了一步操作。我自己会更推荐 findall因为它天然跳过空白和分隔符行为稳定新手不容易写出隐蔽bug。2.2 统计频率字典用法的三档进化拿到单词列表之后接下来的任务就是统计每个单词出现的次数。这个需求用字典解决是天经地义的但写法的好坏直接关系到代码的长短和可读性。最基本的方法是判断键是否存在word_count {} for word in words: if word not in word_count: word_count[word] 1 else: word_count[word] 1这段代码逻辑完全正确但非常啰嗦每次循环都要做一次键是否存在判断。换成字典的 get 方法能精简到两行word_count {} for word in words: word_count[word] word_count.get(word, 0) 1dict.get(key, 0) 的含义是如果字典里有这个键就返回它的值如果没有就返回默认值0。所以每次循环先取回当前次数取不到就是0然后加1再写回字典。这行代码是Python社区里公认的优雅写法简洁、高效、易读。第三档则是使用 collections.Counter。这是Python标准库中专门为计数场景设计的类尤其在词频统计这种任务里属于官方外挂。使用方式非常直接from collections import Counter word_count Counter(words)生成的结果就是一个字典子类行为几乎和普通字典一样可以直接查键取值、遍历键值对。Counter 的底层是用C语言实现的对于大规模文本速度比手动循环更快代码也更简洁。在PTA考试里用Counter是允许的因为collections是标准库不需要额外安装任何东西。需要提醒的是Counter对象虽然可以当普通字典用但如果你要调用 .items() 之后配合 sorted 排序排序规则和普通字典一模一样。后面讲排序时会细说。2.3 排序规则两个条件如何一次搞定词频统计题的输出要求几乎逃不出频率降序、同频字典序升序这个框架。排序代码是整道题的核心也是错误高发区。常见的第一种错误写法是直接调用 sort 或 sorted 加上 reverseTruesorted_words sorted(word_count.items(), keylambda x: x[1], reverseTrue)这样确实把出现次数按降序排列了但次数相同的单词的顺序是按照它们在 items() 中出现的前后顺序排列的而不是字典序。如果题目要求同频单词按字母升序这个写法必挂。正确写法是让 key 返回一个元组把两个排序条件都表达出来sorted_words sorted(word_count.items(), keylambda x: (-x[1], x[0]))这里的原理值得展开讲。sorted 的 key 参数接收一个函数函数接收列表中的一个元素并返回一个用来排序的值。Python在比较值的时候如果值是元组会先比较第一个元素如果第一个元素相等再比较第二个元素以此类推。在这个写法里x[1] 是出现次数x[0] 是单词本身。取负号 -x[1] 是为了让次数越大排序越靠前次数相同时比较 x[0] 这个字符串本身字符串按字典序升序排列。于是两个条件全部满足还不需要 reverseTrue完全符合题目要求。这道题里还会遇到一种情况如果题目只是要求输出频率最高的前几个单词比如前10个你依然可以用同样的key规则配合切片操作。不过如果需要高效处理大规模数据可以考虑使用 heapq.nlargest这个在后面扩展部分会讲。输出格式一般用循环遍历 sorted_words然后格式化字符串for word, cnt in sorted_words: print(f{word}:{cnt})注意f-string里的冒号后面不加空格因为PTA的输出设置通常比较严格凡是题目样例里没出现空格的地方就不要加加了就是Presentation Error格式错误等于是白写法。别小看这种细节PTA是存在格式错误的很多同学代码完全正确却拿不了满分就是败在这种地方。3. 完整代码实现与逐段解析3.1 一份可以拿去提交的完整参考代码所有思路理顺之后完整代码其实很短大概20行左右。下面这份代码是按最常见的输入输出格式写的也就是输入多行英文文本以单独一行的#结束输出单词:次数格式按次数降序、同频再按字典序升序。import sys import re from collections import Counter text for line in sys.stdin: if line.strip() #: break text line words re.findall(r[a-zA-Z], text.lower()) word_count Counter(words) result sorted(word_count.items(), keylambda item: (-item[1], item[0])) for word, cnt in result: print(f{word}:{cnt})这段代码在PTA上可以直接提交但你需要先确认自己题目里的输入输出格式和示例是否一致。如果输入的结束标志不是#而是别的字符或者输出格式是单词 次数中间空格分隔只需在对应位置稍作修改即可。千万不要盲目复制先看懂每一行在干什么。3.2 逐行解读每个细节都有它的理由第一行和第二行导入模块。sys用于读取标准输入re用于正则匹配Counter用于统计频率。 import 语句放在文件最顶部是Python的代码规范同时也方便你一眼看出程序用到了哪些库。第4到第7行处理输入。用 sys.stdin 迭代读取每一行然后检查当前行是否只有#字符。line.strip() 的作用是去除这一行首尾的空白字符包括换行符这样才能把#行正确识别为终止标记。这里有个常见问题如果题目约定结束标志不是单独一行而是文本中的某个字符比如一行中任何位置有#就结束那判断逻辑就得换。一个更省事的办法是直接读全部文本再分割text sys.stdin.read().split(#)[0]sys.stdin.read() 会把整个标准输入读成一个长字符串然后按#分割取第一段。这种方法在处理PTA输入时非常常用但如果题目里的文章本身包含#字符就要小心好在词频统计的背景通常是英文文章一般不会出现#用 split(#)[0] 反而更简洁。第9行是核心之一。text.lower() 先把所有字母转成小写然后 re.findall(r[a-zA-Z], ...) 提取所有纯字母组合。注意正则表达式里没有大写的A-Z因为上下文中文本已经全转小写但为了防御万一写上 [a-zA-Z] 不会出错还是保留大小写更稳妥。第10行 Counter(words) 一步完成频率统计。之前提过如果不想用Counter手写字典也完全可以word_count {} for w in words: word_count[w] word_count.get(w, 0) 1两种方式在PTA上都能过区别只是代码风格和运行效率。第一次写题建议把两种方式都理解透这样就算考官不让你用Counter基本不会你也能手写出来。第12行是排序。keylambda item: (-item[1], item[0]) 这个lambda表达式的逻辑在前面已经解释过这儿再补充一个细节sorted 返回一个新的列表不会修改 word_count 本身。如果后续还有操作 word_count 的需求这个特性就很友好。第13到第14行是输出循环。f-string 格式化字符串是Python 3.6以上的特性PTA的Python编译器通常是支持的。如果你遇到的是老版本Python 3.5或更早需要改成 {}:{}.format(word, cnt) 或 %s:%d % (word, cnt)不过现在基本不会出现这种情况。3.3 如果题目加了只输出前N个怎么办有些版本的词频统计题会要求输出出现频率最高的N个单词和次数N可能是个固定的数字比如10也可能是从输入中读入。如果遇到这种情况上面的代码只需要在输出时加一个切片for word, cnt in result[:10]: print(f{word}:{cnt})当 N 比较大时切片没问题但当 N 很小且数据量很大的时候可以先对全部数据进行排序再做切片性能上不算最优。更优的做法是使用 heapq.nlargest只维护一个大小为N的堆避免全量排序。以Top-10为例import heapq top10 heapq.nlargest(10, word_count.items(), keylambda item: (item[1], item[0]))但要注意heapq.nlargest 默认是从大到小选取如果你直接把次数和单词放进key元组而不做负号处理词典序会变成单词从大到小的顺序而不是题目要求的升序。这时候你需要把key写成 (item[1], item[0])但结果顺序会变成次数降序、同频单词降序要想完全符合题目依然需要用 (-item[1], item[0]) 作为nlargest的key。这个细节非常坑很多人没注意到。对PTA的30分题来说数据规模一般不会特别夸张直接用 sorted 全量排序然后切片是最稳妥、最不易出错的方案。追求性能优化当然好但先把正确性保证了更重要等你有余力再去研究大数据的处理方案。4. 常见错误与调试技巧我见过的高频WA原因4.1 学生最常见的几个写错点和对应的排查思路每次讲解这道题都会有同学带着代码来找我报错信息五花八门但归根结底错误原因就那么几类。这里我按出现频率从高到低整理成一个速查表方便你在提交前自查错误现象可能原因解决办法同频单词输出顺序反了排序用了reverseTrue导致字典序也被逆转改为 keylambda x: (-x[1], x[0])不要用reverseThe和the被统计成两个词没有先统一小写提取单词之前先执行 text.lower()Python3变成了python3或拆出数字正则用了 r\w 而不是 r[a-zA-Z]把 \w 换成集合本身只保留字母输出里出现空字符串用了re.split且没有过滤空串改用re.findall总是不输出结果或报错输入终止条件判断错误比如多读了一行用 print(repr(line)) 检查实际读到的内容格式错误Presentation Error在冒号后加了空格或者用了逗号而不是冒号严格按题目样例的输出格式来一个小数点都别改这个表里的前两条就能解释绝大多数同学的WA。尤其是第一条很多人在理解了次数降序之后自然想到加一个 reverseTrue结果同频单词的顺序就反了。他们看到输出顺序不对又找不到原因因为循环没问题、统计没问题、字典没问题问题就在那个不起眼的 reverse 上。我在调试时遇到类似的排序问题习惯是先打印出排序后的前10项仔细观察一下被 sort 处理过的列表到底是什么顺序。python交互式环境里跑一下眼见为实比盯着代码猜快得多。4.2 动手构造测试用例比反复提交更有效的检查方法在PTA刷题最大的陷阱就是你提交一次要等系统反馈一次反馈得慢不说还容易让人养成错了再试的坏习惯。更高效的策略是自己在本地构造一些边界测试用例提前把系统可能考察的情况全部跑一遍。我这里分享几组完全合格的测法覆盖常见边界条件。第一组正常文本加大小写混合。输入: Hello world. HELLO Python! # 期望输出: hello:2 python:1 world:1注意hello出现两次frequency最高排第一python和world都是一次按字母顺序python在前world在后。第二组标点连续和数字混入。输入: Python,Python...Java123!!! Java? # 期望输出: java:2 python:2这个用例特别适合测试正则提取。如果代码里用的是 r[a-zA-Z]得到的单词列表是[python, python, java, java]完全正确如果用的是按空格或按标点切分的方法极容易把Java123或Python,拆错。第三组空输入或只有终止符。输入: #按照题目约定没有单词自然没有输出程序应该正常结束不报错。如果代码写了循环输出但没有检查空列表有可能在遍历时没有元素导致异常或者 print 一些奇怪的东西。一个健壮的程序面对这种输入应当安静地退出。第四组多行输入。输入: This is a test. This is only a test. # 期望输出: is:2 test:2 a:2 this:2 only:1这里This在不同行里出现但大小写统一后都是一个this四种频率为2的词按字典序排列是 a, is, test, this。注意字典序中 is 排在 this 前面因为i在t前面而a又在i前面。多推一步你会发现only虽然是最后一行出现的但因为它频率最低被排到了末尾这也能检验排序代码是否正确。我在实际调试时有个习惯就是把这四组测试输入保存成txt文件然后运行代码并加一条 print(re.findall(...)) 或者把word_count打印出来对照中间变量是否和自己心算结果一致。这个分步打印验证法尤其是print大法是所有Python调试题里最有效、最直观的手段没有之一。4.3 在线评测系统的那些玄学问题有些同学会碰到一种情况本地跑得好好的一提交就是全错而且不是运行错误是逻辑错误。这种玄学问题多半不是代码逻辑本身的问题而是输入读取方式的差异在作祟。PTA的输入是标准输入流如果你用了 input() 一行一行读取但题目实际输入是很多行长文本那么 input() 竟然也可以正常工作只要输入行数不超过系统限定的行数。但如果文本行数特别多或者某行特别长用 sys.stdin 这种按流读的方式会更稳定不会在行数多到一定程度时报错。另一个容易被忽略的是文本末尾换行符。用 sys.stdin.read() 读全部文本时文本末尾可能带着一个或两个换行符strip 和 split 的组合在此处显得格外重要。如果你的正则表达式已经足够健壮末尾的换行符根本不会算作单词所以这一步最需要检查的是终止符判断而不是换行符过滤。在我自己的做题经验里只要能够构造出边界用例的本地测试全部通过提交到PTA上的成功率基本是十拿九稳。调试的本质是验证程序在每个输入分支上都行为正确而不是祈祷在线评测系统放过你。5. 从这道30分题延伸出去词频统计背后的真实场景5.1 常见的变体与进阶玩法词频统计这道题在PTA里是独立的30分题但在真实世界的编程任务里它往往只是更大流程的一环。很多同学做完了之后觉得哦不过如此但实际上只要稍微调整需求难度会进一步上升。最常见的变体是增加停用词过滤。英文里a、an、the、is、of这类词出现频率极高但对文本主题没有太大参考价值在做自然语言处理任务时通常需要把它们过滤掉。这道题的某个版本里也可能要求忽略长度小于3的单词或者过滤停用词写法也很简单stopwords {a, an, the, is, of, and, to} filtered_words [w for w in words if w not in stopwords and len(w) 3]这个列表推导式本身不难但加上之后你对题目的理解就深了一层开发环境里的代码不是能过就完还要思考怎么让结果更贴近真实需求。另一个变体是输出频率最高的N个词前面提过用heapq.nlargest。再进阶一些甚至可以尝试用生成器处理超大型文本一行一行读入逐行更新Counter避免一次性把所有文本加载进内存。虽然PTA题目不会刁难到这个程度但你能意识到这是性能瓶颈说明你已经开始用工程师的角度思考问题而不是做题学生的角度了。5.2 这道题与真实编程世界的联系不白做的题目很多学生觉得PTA题就是拿来过关的跟真实项目没什么关系。这话对一半。词频统计这个任务本身在搜索引擎的页面打分、推荐系统的关键词抽取、新闻热点发现、甚至聊天机器人意图识别里都出现过相似的身影。我举个最简单的例子你现在打开任何网站的全文搜索功能后台第一步做的事就是把文档里的词频统计出来然后根据每个词在文档里出现的频率结合其他信息计算权重。虽然实际实现比这道题复杂得多但核心的文本切分、大小写归一化、频率统计、排序四大步是一模一样的。学会这道题等于是在给后面的数据分析、自然语言处理打基础。从带作业的角度我也再分享一个经验给同学讲这道题时发现真的理解了字典处理逻辑的人后面讲JSON数据遍历、讲pandas分组聚合时明显上手更快。因为它们的底层思维都是键值对映射加分组归纳这道30分题本质上就是把这个编程思维的基础练扎实了。最后说点实在的。如果你现在正在做这道题先别急着提交把输入输出格式反复看三遍再把边界用例在本地跑一遍最后再上线评测。磨刀不误砍柴工这十分钟的准备省下来的可能是十几次提交失败带来的挫败感。 我个人带过不少同学做这道题最深的体会是这题的难点压根不在统计这两个字上而在所有你认为这不就是很简单吗的细节里。细节决定AC一点都不夸张。
阅读完成 · 觉得有帮助?
咨询建站