拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

零基础学Python必刷的两道题:李白打酒与词频统计

零基础学Python必刷的两道题:李白打酒与词频统计

前两天在社群里看到有人问:"零基础学Python到底刷什么题?我打开LeetCode,第一题就看不下去。"这个问题我太有发言权了。我带过的零基础同学,几乎全是从同一组题入门的:李白打酒和英文词频统计。这两道python编程题一道练逻辑、一道练数据,难度刚好卡在"能看懂、要动脑、写完有成就感"的位置。文章不长,但我把从读题、推导、写代码到踩坑的完整过程都放进来了,无论你是刚开始学,还是已经会写点基础语法想找手感,都可以按这个流程走一遍。

1. 为什么是这两道题:一本逻辑题一本数据题

1.1 李白打酒:一道连初中生都能看懂、程序员却能翻车的题

李白打酒是网上流传很广的一道古典算术题,原文大概是:"李白无事街上走,提壶去买酒。遇店加一倍,见花喝一斗。三遇店和花,喝光壶中酒。试问壶中原有多少酒?"

这道题最妙的地方在于:题目本身只有几句话,不需要任何专业背景,小学文化就能读懂。但真正动手写出代码后,你会发现自己面临的不只是"算出来"这么简单,而是怎么把文字逻辑转成循环和分支结构,怎么处理"最后一次状态已知,初始状态未知"的逆向推导。很多人第一反应是设未知数列方程,但题目换一个表述,比如"遇店加两倍""四遇店和花",方程就乱套了。用代码来解决,其实更通用、更稳。

1.2 词频统计:把"工程能力"藏进一道小学题里

英文词频统计这题看起来也简单:给你一段英文文本,统计每个单词出现多少次,输出出现次数最多的前几个单词。但真正动手做的时候,你会同时遇到字符串清洗、大小写归一化、字典计数、排序、切片五个基础任务。这些任务单独拿出来都不难,合在一起偏偏能卡住一大片初学者——不是语法不会,而是"先干什么后干什么"的流程感没有建立。

这两道题放在一起,恰好覆盖了Python学习路径上最重要的两条分支:一条是逻辑推导,一条是数据处理。前者训练你的脑子,后者训练你的工程手感。很多教程让你刷五十道题,其实不如把这两道题老老实实推一遍、写一遍、改三遍。

1.3 两道题覆盖的语法点对照

我做了个简单的对照表,方便你自查:

知识点李白打酒词频统计掌握等级
循环结构倒推循环、穷举验证遍历单词列表必会
分支结构判断遇店还是遇花判断字符类型必会
函数定义函数封装与参数传递构造可复用统计函数必会
列表操作正向序列模拟split切分、列表推导必会
数组切片不涉及items[:top_n] 取前三必会
字典操作不涉及单词计数、get默认值必会
类型转换整数和浮点数、分数lower、字符串与列表常踩坑
正则/高级模块不涉及re模块、Counter进阶

如果你能不看答案把这两道题都写出来,说明你至少可以独立开始做小型数据处理和简单的算法练习题了。

2. 第一道:李白打酒,逆向递推的完整推导

2.1 题意里的那个坑:"三遇店和花"到底怎么理解

我第一次做这道题就栽在了题意理解上。题目说"三遇店和花",很多人的第一反应是先遇三次店,再遇三次花。但细想一下,如果连着遇到三次店,酒量翻倍翻倍再翻倍,再连续喝三次,也能喝光,但这不符合"见花喝一斗"的随机性。更符合文字原意的理解是:李白每走一段路遇到一次店,酒量加倍,走一段路遇到一次花,喝掉一斗,这个过程重复三次,也就是操作顺序是"店、花、店、花、店、花"。

这个理解非常关键,因为它直接决定你要写的循环结构是什么样。市面上有些版本的题把"三遇店和花"解释成一次店一次花算"一遇",三次就是三种店花交替,那代码里的循环就要写成偶数次操作。还有版本说"遇店加一倍,遇花喝一斗"不分顺序,随机遇到六次。所以拿到编程题的第一步,永远是把题意中的隐含规则敲死,不然代码写得再漂亮都是错的。

2.2 从"喝光壶中酒"往回收:逆向推演

已知最后酒量为0斗,最后一步是见花喝一斗。那我就把整个流程倒过来看:原来"遇店加一倍"的逆操作是"除以2",原来"见花喝一斗"的逆操作是"加上1斗"。从最终状态0倒着往前推,遇到花的逆操作就加一,遇到店的逆操作就除以2。

具体推一下:

  • 初始未知,假设为x。
  • 第一次遇店:酒变2x。
  • 第一次见花:酒变2x-1。
  • 第二次遇店:酒变4x-2。
  • 第二次见花:酒变4x-3。
  • 第三次遇店:酒变8x-6。
  • 第三次见花:酒变8x-7 = 0。

解得x = 7/8斗。也就是说,李白出门时壶里只有不到一斗酒。

用逆向推更直接:最后一次喝光前,酒量必须是1斗;再往前推一步,遇店加一倍之前,酒量必须是0.5斗;继续往回收,最后能得到7/8斗。这个过程用循环来表达,比解方程更贴合编程思维。

2.3 一口气写出能跑的版本

我习惯先把操作序列显式写出来,再去写循环。代码如下:

sequence = ["店", "花", "店", "花", "店", "花"] wine = 0.0 for step in reversed(sequence): if step == "店": wine = wine / 2 # 遇店加一倍,倒推时除以2 else: wine = wine + 1 # 见花喝一斗,倒推时加回一斗 print(wine) # 输出 0.875

这里用reversed(sequence)把正向流程倒过来遍历。我每次给同学讲这段代码,都要强调一点:倒推时先判断是店还是花,再决定做除法还是加法,别把顺序写反了。有人直接把操作序列写成["花", "店", "花", "店", "花", "店"]再正向遍历,效果是一样的,但思路一定要说清楚。

2.4 把解法打包成函数,再写一个正向验证

光算出7/8斗还不够稳,我还会加一个正向验证函数,从7/8开始模拟六步,确认最后结果为0。这也是老手写代码的习惯:逆向算完,正向验证,双重保险。

def li_bai_wine(rounds=3): """通过逆向递推计算初始酒量""" wine = 0.0 for _ in range(rounds): wine += 1 # 逆花 wine /= 2 # 逆店 return wine def verify_wine(initial, rounds=3): """正向模拟店花交替,验证酒是否喝光""" wine = initial for i in range(rounds * 2): if i % 2 == 0: wine *= 2 # 遇店 else: wine -= 1 # 见花 return wine init = li_bai_wine(3) print(init) # 0.875 print(verify_wine(init)) # 0.0

这里当i % 2 == 0时执行遇店操作,刚好模拟"店花店花店花"的顺序。如果把rounds=3换成4,就变成"四遇店和花",代码不需要大改,这就是封装成参数的价值。你在写这类题的时候,也可以试着把可变的部分全部抽成参数,以后题目稍微一变,你改一行就行。

3. 第二道:英文文本词频统计Top N

3.1 需求看起来简单,拆开其实有四步

词频统计的核心需求一句话就能说清楚:输入一段英文文本,输出出现次数最多的前N个单词。但这句话背后藏着四个独立步骤:清洗文本,把标点符号、换行符去掉;统一大小写,让"Python"和"python"算同一个词;切分单词并计数,用字典记录每个词出现次数;排序并切片,按频次降序拿到Top N。

我见过太多人直接拿一个变量count从头数到尾,数到一半发现单词太多了根本没法数。这是因为没有先把"整体流程"拆成"输入、处理、输出"三个环节。写代码之前,先问自己三个问题:输入长什么样?中间要做哪些变换?最终要得到什么格式的结果?这一步想清楚,后面基本不会跑偏。

3.2 第一版实现:换标点、切词、字典计数、排序切片

我给出一个很朴素的版本,完全不用任何高阶模块,适合零基础理解:

def top_words(text, top_n=3): # 第一步:把标点和换行统一替换成空格 for ch in ",.!?;:()'\"-\n": text = text.replace(ch, " ") # 第二步:统一小写并切分成单词列表 words = text.lower().split() # 第三步:字典计数 freq = {} for word in words: freq[word] = freq.get(word, 0) + 1 # 第四步:排序,频次高的在前,频次相同按字母顺序 items = sorted(freq.items(), key=lambda x: (-x[1], x[0])) # 第五步:切片取前 top_n 个 return items[:top_n]

测试用的文本我故意写了一个需要清洗的版本:

sample_text = """ Python is powerful, Python is easy, and Python is fun. Python is also popular. Python makes data science easier! """ for word, count in top_words(sample_text, 3): print(f"{word}: {count}")

运行结果:

python: 5 is: 3 and: 1

注意第三名的处理。频次为1的词有一堆,我给排序规则加上了(频次降序, 字母升序),所以and排在了easy、fun、popular前面。如果你不在意并列词的顺序,也可以只用key=lambda x: x[1]加一个reverse=True,但推荐做成稳定的排序,方便复现结果。

3.3 借这道题把切片和排序的细节彻底搞明白

items[:top_n]这行代码就是数组切片最典型的用法:取列表前N个元素。切片的完整语法是列表[start:stop:step],省略start表示从开头取,省略stop表示取到结尾,[:3]就是取索引0、1、2三个元素。很多教程把切片讲得很玄,其实你把它理解成"复制一份列表的指定区间"就行。

排序这里其实还有个小坑:freq.items()返回的是字典的键值对视图,直接对它调用sorted()会得到一个按字典序排序的键值对列表,而不是按频次排序。所以必须用key参数告诉Python按哪个字段排。lambda x: (-x[1], x[0])的意思是,对每个元素x(一个元组),x[1]是频次,x[0]是单词,频次取负就能实现降序,频次相同时再按单词升序。这个技巧在数据分析、日志统计里特别常用,值得记下来。

3.4 换用Counter和正则,代码少一半

当你理解了上面的基础版,就可以换个更高效的写法了。Python的collections.Counter专门用来统计可哈希对象的频次,re.findall可以一次性提取所有符合条件的单词:

from collections import Counter import re def top_words_fast(text, top_n=3): words = re.findall(r"[A-Za-z']+", text.lower()) return Counter(words).most_common(top_n)

这段代码只有三行,但做的事和前面的函数一模一样。re.findall用正则表达式把字母和撇号全部挑出来,跳过数字和其他符号,text.lower()统一转为小写,Counter(words).most_common(top_n)一行完成计数和取TopN。如果你在别人的项目里看到这种写法,别懵,它背后就是刚才那五步的压缩版。

3.5 我在这道题上见过最多的三类错误

第一类:不做大小写归一化。"Python".lower()变成"python",否则同一个词会被统计成两个词。第二类:没清洗标点,"fun."和"fun"会被当成两个词,导致明明只有一个单词却出现两次。这类错误特别隐蔽,因为你看输出时容易忽略句号的存在。第三类:手动计数时忘了处理字典中不存在的键,直接写freq[word] += 1,程序一运行就报KeyError。正确姿势是freq.get(word, 0) + 1,get方法在键不存在时返回默认值0。

我建议新手第一版用最原始的方式逐步写出,看看每一步的输出是什么,比如先只执行text.lower().split()并打印,再执行计数并打印,最后再排序。逐步调试一遍,比你闷头写十遍都有用。

4. 两道题背后的通用解题框架和排坑清单

4.1 五步解题流程,两道题都在验证同一套打法

这两道题看完了,你会发现它们的解题流程惊人地一致。我已经把这套流程固定成了做编程题的五步,分享给你:

  1. 列出已知条件。李白打酒已知终态为0、店花交替三次;词频统计已知输入是文本、输出是前N个单词。把条件列出来,等于把题目翻译成了程序语言的变量。
  2. 确定核心数据结构。李白打酒需要一个浮点数变量存酒量;词频统计需要一个字典存频次。数据结构选对了,写出来就是顺畅的,选错了就会越写越乱。
  3. 先写最朴素的版本。不要一开始就想"我要写得多优雅",先用if加for把逻辑跑通,结果对了再谈优化。
  4. 考虑边界情况。词频统计里空文本怎么办,只有标点没有单词怎么办;李白打酒里rounds传0会怎样。加了边界处理,你的代码才敢放到真实场景。
  5. 反向验证结果。李白打酒逆向算完接着正向跑一遍,词频统计打印一下原始words列表看一眼。验证不是浪费时间,是让你睡个好觉。

4.2 高频BUG复盘表

我在答疑过程中收集了一些初学者特别容易踩的坑,做成表格供你对照:

错误类型典型表现原因解决方式
索引越界IndexError: list index out of range循环里取words[i+1]没判断长度用for word in words遍历,少用手动索引
变量覆盖统计结果永远是0或1在循环体里重置了计数变量打印循环内变量值,检查语句缩进层级
类型拼接TypeError: can only concatenate str试图用+拼接字符串和数字先str(count)再拼接,或用f-string
原地删除遍历列表时删元素导致跳过for w in words里直接remove(w)改用新列表保存结果,或倒序遍历
字典键不存在KeyError对不存在的键做运算用get(key, default)
大小写不统一"Python"和"python"两个词没有调用lower()切词前统一小写

这里特别提一下for w in words里直接remove的问题,很多人觉得这是小问题,实际上它会让你漏掉元素,因为删除后列表索引前移,循环却还在按原索引向后走,后面一半元素会被跳过。我的建议是:要过滤,就构造新列表,别在原列表上边遍历边删。

4.3 写代码时让自己像"老手"的几个小习惯

老手和新手写的代码,在能跑的前提下,最大的区别是三个:一个有名字,一个全是a、b、c;一个能分步骤验证,一个只能一把梭子从头跑到尾;一个主流程清晰,一个所有逻辑全堆在全局代码里。

所以我的建议是:变量名用word、freq、wine,不用x、y、z;每个关键步骤后加一个print看一眼中间值,确认没问题再继续;把核心流程封装成函数,主程序只留输入输出。这些习惯现在看起来有点浪费时间,但等你开始写超过一百行的脚本、需要协同调试的时候,你就知道它们有多值钱了。

5. 做完这两道题,接下来往哪走

5.1 李白打酒的变式训练

想要把这道题的价值榨干,最简单的变式就是改参数。把"三遇店和花"改成"四遇店和花",测试你封装的函数能否直接复用;把"见花喝一斗"改成"见花喝两斗",检查逆向递推的加一操作是否改对;把"遇店加一倍"改成"遇店加百分之五十",验证倒推时除以1.5而不是除以2。这些变式不需要新知识,但能帮助你彻底掌握逆向递推的边界条件。

如果还想进阶,可以试试把正向模拟做成一个可交互程序:输入初始酒量,程序模拟每一次遇店遇花的过程,实时打印壶中剩余酒量。这样一个命令行小工具就出来了,比单纯做数学题有趣得多。

5.2 词频统计的升级方向

词频统计的升级方向就很鲜明了。第一步是换成中文文本,Python处理中文需要先分词,常用的是jieba库,切出来的词存进列表之后,后面的字典计数、排序、切片逻辑完全不变。第二步是结合数据可视化,把Top10单词画成柱状图,用matplotlib几十行就能出来一张漂亮的图。第三步是做一个"文件版词频统计",支持传入一个txt文件路径,自动读取全文并返回统计结果,这样它就不再是练习题,而是一个可以用起来的工具。

我个人的经验是,一道题刷完,至少做一次变式、一次场景升级,才算真的吃透了。很多人刷题只求"做出来",做完就扔,过两周再看代码跟看天书一样。你试着把这两道题改一改、扩展一下,你对"代码还能这么写"的体会会比刷十道新题还深。

这两道题最大的价值,不在于它们本身有多难,而在于它们逼你走完一遍"读题、拆解、实现、验证、优化"的完整流程。我带过的同学里,凡是认认真真把这两道题写成函数、加好验证、跑通变式的,后面学Python都明显顺利很多。编程这东西,最终拼的不是记了多少API,而是你把多少基础过程变成了手感和直觉。这两道题,就是练手感和直觉的起点。

返回列表