刚学 Python 的那段时间,我印象最深的不是语法,也不是课后概念题,而是《Python语言程序设计基础》这本教材第4章“程序的控制结构”后面的程序练习题。这一章的练习题看着都不长,但每一道都在逼你从“认识语法”切换到“设计程序”的思维方式。我见过不少同学卡在这一章,有的是循环边界搞不清,有的是不知道从哪里下手拆题。这篇文章我就来聊聊第4章程序部分练习题的解题思路,并给出常见题型的参考代码和避坑经验,希望能帮正在啃这一章的人少走弯路。
1. 第4章在整本教材里到底是什么位置
1.1 这一章的知识点到底有哪些
第4章的标题是程序的控制结构,核心内容基本围绕三条线展开:顺序结构、分支结构、循环结构。看起来就三个词,但练习题却覆盖了无数变化。
顺序结构没什么好说的,就是从上到下按行执行。考试和练习题很少单独考顺序,它藏在所有题目的基础里。分支结构对应的是 if / elif / else,练习题基本离不开条件判断,比如判断成绩等级、判断闰年、判断奇偶、实现简单的菜单选择。循环结构是重头戏,包括 for 循环、while 循环、break、continue、pass,以及嵌套循环。练习题里大量出现的素数判断、水仙花数、九九乘法表、百钱买百鸡,全是循环的变体。
教材在章节前半部分讲语法,后半部分就是这些程序练习题。很多人觉得语法看懂了,代码却写不出来,就是因为练习题需要的不只是语法,而是把数学问题翻译成流程控制的能力。这个能力只能靠做题来练,看再多的语法解释都没用。
1.2 为什么第4章练习题值得认真做
我个人的看法是,这一章是整本教材的分水岭。前面几章学的是数据类型、变量、运算符,你可以靠记忆去应对,到了第4章,记忆开始不管用了,你真正需要的是“把一个具体问题分解成计算机能执行的步骤”的能力。
举个很简单的例子:输出100到999之间的水仙花数。这道题表面上考的是一位数、十位数、百位数的提取方法,本质上考的却是“如何用一个循环遍历所有三位数,再在循环体里逐个判断条件”。这个思维一旦建立,后面学函数、列表、字典,甚至文件操作,都是在这个骨架上去添砖加瓦。反之,如果第4章是混过去的,后面章节的程序填空题和综合应用题会越来越吃力。
另外,很多计算机等级考试级的基础题,甚至一些学校期末考试的编程题,都是从这一章的题型演变出来的。把这一章的题目吃透,等于提前给期末复习打了个底子。
2. 必练题型逐题拆解与参考实现
2.1 水仙花数:数字拆解是基础中的基础
水仙花数指的是一个三位数,其各位数字立方之和等于该数本身。比如153就是一个水仙花数,因为 1的三次方 + 5的三次方 + 3的三次方刚好等于153。
常规做法用一个 for 循环遍历100到999,每次循环里取出个位、十位、百位,计算立方和再判断是否等于原始数字。取位方法其实很简单:
for num in range(100, 1000): hundreds = num // 100 # 整除取百位 tens = (num // 10) % 10 # 先整除10,再取余,得到十位 ones = num % 10 # 取余得到个位 if hundreds**3 + tens**3 + ones**3 == num: print(num)这里有不少新手会搞混整除和取余的优先级。简单说,整除运算是向下取整,丢掉小数部分。一个三位数除以100再取整,自然得到百位数。十位数的提取是先整除10,把个位扔掉,再对10取余,留下来的就是原数的十位。个位最简单,直接模10就行。
这段代码跑出来是四个数:153、370、371、407。很多参考答案还会要求控制输出格式,比如逗号分隔。这时候可以用 join 或者先存到列表里再拼接,也可以用一个标志变量控制逗号的输出。练习题里常见的坑反而是“输出最后一个数后不能有多余逗号”,这个问题在最开始的版本几乎都会被问到。
2.2 素数判断:循环边界条件决定成败
素数判断是这一章出现频率最高的题型。题目通常有两种版本:第一种是输入一个数,判断它是否为素数;第二种是输出 m 到 n 之间的所有素数。
判断素数的核心逻辑不复杂:一个大于1的自然数,除了1和它本身,不能被其他数整除。最简单的实现就是遍历2到num-1,看是否存在能整除它的数。
但教材练习题里经常会问“优化”这个角度。一个很基本的优化是:只需要检查到 int(math.sqrt(num)) + 1 即可。理由也很直观,如果 num 有一个大于其平方根的因子,那么必然存在一个小于其平方根的对应因子,所以检查到平方根就已经覆盖了所有可能的“较小因子”。
参考代码如下:
import math def is_prime(n): if n < 2: return False for i in range(2, int(math.sqrt(n)) + 1): if n % i == 0: return False return True # 输入范围并输出素数 start = int(input()) end = int(input()) for n in range(start, end + 1): if is_prime(n): print(n, end=' ')这题我见过最多的问题有两个:一个是忘记处理小于2的情况,比如1会被误判为素数;另一个是循环上界写成 n,虽然没有错,但效率低,而且数值稍大时能明显感觉到卡顿。练习题如果要求输入一个大数范围,优化版和不优化版的差别还是很明显的。
2.3 九九乘法表:嵌套循环与格式控制的结合
九九乘法表几乎是所有教材第4章的标配题目。它考的本质是嵌套 for 循环,以及 print 函数的输出控制能力。下面是一个最常用的版本:
for i in range(1, 10): for j in range(1, i + 1): print(f"{j}*{i}={i*j}", end="\t") print()要点在于内层循环的范围是 range(1, i+1),保证乘法表呈现下三角形状。如果写成 range(1, 10),就变成完整的9×9矩形了,虽然也算乘法表,但不符合通常题目要求的“下三角”。
另一个容易忽略的地方是每行的换行。内层循环结束后必须有一个 print() 来换行,否则所有结果会挤在同一行。end="\t" 是为了让列对齐,看起来更整齐。
如果你用的是教材配套环境自带的输出框,可能还会遇到编码问题,比如中文引号或制表符显示异常。这种时候可以把 end="\t" 改成 end=" ",用空格分隔也能接受。
2.4 百钱买百鸡:枚举法的经典应用
百钱买百鸡是另一道经典循环练习题。题目大意是:公鸡5文钱一只,母鸡3文钱一只,小鸡1文钱三只,用100文钱买100只鸡,问公鸡、母鸡、小鸡各多少只。
这道题最直白的思路是三层循环枚举公鸡数、母鸡数、小鸡数。理论上讲公鸡最多20只,母鸡最多33只,小鸡最多100只。三层循环加判断就能得到答案:
for x in range(0, 21): for y in range(0, 34): z = 100 - x - y if 5*x + 3*y + z/3 == 100 and z % 3 == 0: print(f"公鸡{x}只, 母鸡{y}只, 小鸡{z}只")这里有个很关键的优化:因为鸡的总数是确定的100只,所以一旦公鸡和母鸡的数目定了,小鸡的数目就是100 - x - y,不需要第三层循环再遍历一遍。很多参考答案用的就是这个方法,我自己写的时候也经历了从三层循环到两层循环的转变。这个转变很重要,因为它体现出一种“先找数学关系,再写代码”的思考方式。
再注意一点,因为小鸡1文钱三只,用 z/3 在 Python 里得到的是浮点数,判断是否等于100可能会有浮点数精度问题,所以最好加上 z % 3 == 0 这个整数条件。这算是这道题里的一个隐藏考点。
2.5 猜数字游戏:循环加随机数的组合玩法
猜数字是第4章少有的一道“有点游戏感”的题目。程序先随机生成一个1到100之间的整数,然后让用户猜,程序根据用户输入的数字提示大了还是小了,直到猜中为止。
这个题考的是 while 循环的典型用法:循环条件不固定,需要依靠内部的 break 或者标志变量来退出循环。参考实现如下:
import random target = random.randint(1, 100) count = 0 while True: guess = int(input("请输入你猜的数字: ")) count += 1 if guess > target: print("大了") elif guess < target: print("小了") else: print(f"恭喜你猜对了,一共猜了{count}次") break这个例子值得注意的有两点。第一点是 random.randint(1, 100) 和 random.random() 生成的是不同的东西,前者是整数,后者是0到1之间的浮点数。有的练习题要求猜一个浮点数,那就要改成 random.uniform 或者结合精度要求做四舍五入。第二点是 while True 配合 break 的写法,如果只在循环里写判断而不写 break,程序会陷入死循环,校园OJ平台上常见“超过时间限制”的错误,多半就是这个原因。
有的教材会要求统计猜的次数,或者限制最多只能猜10次,这时候就是在循环体加计数器,再加一个 if count == 10 的分支来控制退出。思路一样,只是多几个条件。
2.6 异常处理相关练习题:try-except的入门陷阱
部分教材的第4章练习题会把异常处理内容也放进来。常见题型是输入两个整数,输出它们的商,但程序要能处理除数为0的情况。题目描述可能很长,核心代码却很简单:
try: a = int(input()) b = int(input()) print(a / b) except ZeroDivisionError: print("除数不能为0") except ValueError: print("请输入整数")这里我踩过的一个坑是 try 和 except 的范围。很多新手习惯只把 a / b 放进 try,把 int(input()) 放在外面,这样字符串转整数失败时,异常照样会抛出,代码在本地跑可能没感觉,但练习题要求的所有输入都要不崩溃,这种写法就会在非法输入时报错。正确做法是把所有可能出现异常的步骤都放进 try 块里,分别捕获不同类型的异常。
还有一个细节是 except 后面的异常类型。如果直接写一个笼统的 except: 虽然能捕获所有异常,但不利于排查问题。练习题如果要求“当除数为0时输出提示”,最好显式写成 except ZeroDivisionError,这样逻辑更清楚,评分也不容易误判。
3. 高频错误与排查技巧
3.1 死循环:循环变量没更新导致的卡死
死循环在第4章练习题里非常常见,尤其是 while 循环。最典型的错误写法出现在求和题里,比如计算1到100的和:
# 错误示范 i = 1 total = 0 while i <= 100: total += i # 少写了 i += 1少了 i += 1 这一行,循环条件永远成立,程序就会一直累加下去。这个问题在本地跑的时候表现是控制台没有任何输出,鼠标一直转圈,有的同学还会以为是机器卡了。
排查方法很简单:检查 while 循环体里是否有语句改变循环条件中涉及的变量。如果没有,要么补上更新语句,要么改用 for 循环。我还发现不少同学容易把更新语句放在 continue 之后,比如循环里先用 continue 跳过某些情况,结果 continue 后面的 i += 1 永远执行不到,依然死循环。遇到这种情况,正确的做法是让变量更新发生在 continue 之前,或者把更新语句放到条件判断之前。
3.2 循环边界差一错误:range 的终点是取不到的
range(1, n) 产生的序列是 1 到 n-1,这个规则几乎每个初学者都要栽一次。练习题里经常有“求1到n所有整数之和”这类题,很多人写 range(1, n) 导致少加了一个n。正确的写法是 range(1, n+1)。
差一错误在素数判断里表现得更隐蔽,比如循环范围写 range(2, n),在原题要求判断 n 是否为素数时,这个范围其实是对的,因为不需要检查 n 本身,但如果范围写成 range(2, n+1),程序也能跑,只是多了一次无意义的检查。再比如水仙花数题里,如果循环写成 range(100, 1000),那么起点和终点刚好覆盖所有三位数,写成 range(100, 1001) 也可以,但写成 range(99, 1000) 就会把两位数也包进来。
我自己的经验是,拿到题目先想清楚“我到底要遍历哪些数”,然后用具体数字代入 range 验证一下。比如求1到5的和,如果心里想着 range(1, 6),写出来才对,而不是机械地记一个“range 不包括终点”的结论。
3.3 缩进错误与逻辑层级混乱
Python 用缩进区分代码块,这和第4章里的嵌套循环、嵌套判断高度相关。我在批改同学作业时见过一个高频错误:九九乘法表的内层 print 误放在外层循环里,导致输出结果变成9行单独的乘法算式,而不是下三角矩阵。
缩进错误有时候很难用肉眼发现,尤其是一段代码比较长的时候。排查技巧是利用代码编辑器的缩进辅助线,或者把所有代码统一成4个空格缩进。还有一个很实用的习惯:写完一个嵌套结构后,先看顶层 for 对应的循环体包含哪些行,再看内层 for 对应的循环体包含哪些行。最简单的方法就是在每一层开头加上临时 print 语句,比如 print("外层") 和 print("内层"),运行一遍就能直观看到执行顺序。
3.4 输入输出格式踩坑:int 与字符串的纠缠
这一章的练习题很多要求从键盘输入数据。教材配套练习系统对输入输出的格式要求往往很严格,比如“输入包括一行两个整数,以空格隔开”,不少初学者会这么写:
a = input() b = input()如果输入是“3 5”在同一行,这种写法获取到的 a 等于字符串 "3 5",而不是两个独立的数字。正确做法是用 split 分割:
a, b = map(int, input().split())map(int, ...) 是把分割后的每个字符串都转换成整数,再通过解包赋值给 a 和 b。这个方法在练习题里非常通用,尤其是“一行多个整数”的输入场景。
输出方面的常见问题则是多打了换行或者没换行。比如题目要求输出结果时每个数占一行,有些人没写换行,输出全挤在一起;有些人多写了个空格或逗号,导致格式判定失败。我的建议是按题目给出的输出样例,先在本地手动模拟一遍,确保类型、分隔符号、换行全部一致。
3.5 浮点数比较的坑
第4章练习题引入浮点数之后,会出现一个让很多人懵圈的现象:0.1 + 0.2 的结果不等于 0.3。这在百钱买百鸡这类题里特别容易出现,因为小鸡的价格是 1/3 文,写成浮点数参与比较时,等式两边不相等。
为了避免这种问题,有几种思路。第一种是做数学变形,把等式两边同时乘以3,变成整数运算。比如 $5x + 3y + z/3 == 100$ 可以改成 $15x + 9y + z == 300$,这样就完全没有浮点数精度问题。第二种是使用一个很小的误差范围判定,比如 abs(5x + 3y + z/3 - 100) < 1e-6,在练习题中也常见。我个人的习惯是能用整数就用整数,实在要用浮点数,就把误差阈值写清楚,不要直接用 ==。
4. 刷题方法:从参考答案到独立解题
4.1 拿到题目先“翻译”再写代码
很多人拿到编程题直接开始敲代码,这是效率比较低的做法。我在第4章练习里慢慢总结出一套流程,四个步骤走完,基本不会卡住。
第一步是读懂题目中的数学条件或业务条件。比如“水仙花数”题目里的数学表达式是什么,“素数”的定义是什么,“九九乘法表”的排列规律是什么。把这些条件用文字写下来,或者直接在草稿纸上列公式。
第二步是确定程序的基本结构:这题需要循环吗?循环是固定次数还是不确定次数?如果固定次数,用 for 比较合适;如果次数取决于用户输入或运行状态,用 while 更灵活。判断条件放在循环外还是循环里?是否需要嵌套?
第三步是设计循环体内的步骤。比如水仙花数题目里,循环体内要先取位、再立方求和、再比较。这个顺序其实就是程序流程图的实现。
第四步才是写代码,写完先跑测试数据。常用的测试数据包括:最小边界值、最大边界值、中间值、非法值。比如素数判断,你要测2、3、4、17、1,每个都有代表性。
4.2 一题多解:从暴力和优化中理解本质
第4章的练习题基本都可以用暴力法解决,但平时练习时有必要做一次优化版本。比如前面提到的百钱买百鸡,三层循环可以直接求解,但做完之后可以想一想:小鸡数量能不能通过总数公式直接算出来?如果能,循环层数就从三层变两层。这个过程就是“程序思维升级”的过程。
再比如素数判断,从检查到n到检查到sqrt(n),不只是运行效率的变化,更是对因子对称性的理解。练习题答案只能告诉你“该怎么做”,但你真正要解答的是“为什么可以这么做”。
第4章还有一个常见变式:求斐波那契数列的前n项。这个题至少有两种解法。一种是循环迭代法,用三个变量依次交换实现;另一种是递归法,不过递归在函数章节才会正式引入。如果提前把两种思路都写一遍,等学到函数章节时会觉得非常轻松。
4.3 扩展练习:把课后题改造成小项目
做完教材练习题之后,有一个很好的巩固方法:把题目需求稍微改一改,变成自己的小练习。比如猜数字题目,原题是猜1到100的整数,你可以改成猜1到1000的随机数,并增加“最高成绩”的保存功能,存档到一个文本文件里。这就会牵涉到文件操作,虽然超出了第4章的范畴,但你会提前感受到后续章节知识的用处。
再比如百钱买百鸡,原题要求公鸡、母鸡、小鸡共100只,总价100文,你可以把它改成:鸡的品种增加到4种,总价变成200文,然后用枚举法重新求解。这种改造练习不需要多复杂,关键在于让你自己动手重新组织循环结构,比照着答案看十遍都管用。
我个人辅导过的一位同学,在第4章练题阶段把每个题都做了至少三个版本:标准答案版、优化版、改造项目版。一个月后再做课程设计里的循环命题,基本不用翻书就能直接写出来。刷题的意义从来不在“答案背下来”,而在“熟练地做出判断”。
5. 程序练习题的易错点速查
这一章里有很多细节,教材未必逐个强调,但练习系统里很容易被扣分。我把常见问题整理成一个速查表,方便你在提交代码前逐项检查:
| 问题类型 | 典型场景 | 参考解法 |
|---|---|---|
| while 死循环 | 循环变量未更新,或更新在 continue 之后 | 检查循环体内所有可能执行的路径,更新语句放在 continue 之前 |
| for 循环边界或范围 | range 终点取不到,导致少循环一次 | 明确遍历对象范围,用具体数字代入验证 |
| 缩进错误 | 多层嵌套的 print 放在错误的层级 | 统一4空格缩进,用临时 print 辅助定位 |
| 输入格式不匹配 | 一行多个整数却用多次 input() 读取 | 使用 input().split() + map(int, ...) |
| 浮点数比较 | 0.1+0.2 不等于 0.3 | 等式两边乘以公倍数转为整数运算,或设置误差阈值 |
| 除零异常 | 输入0作为除数,直接崩溃 | try-except 捕获 ZeroDivisionError |
| 输出格式错误 | 末尾多逗号、缺少换行 | 对照题目输出样例,用列表拼接后输出 |
这个表格看起来简单,但每一条的背后都是一次运行时错误或者一次练习系统提示的“答案错误”。我刚开始刷这一章时,在“输出末尾多逗号”这个问题上翻过好几次车。当时是用 end="," 把所有结果拼到一行,结果最后一个数后面跟着逗号,练习系统判定格式错误。后来我改成把结果存进列表,再用 ",".join(map(str, results)),这个问题就再没出现过。
还有一个小技巧:如果练习系统支持在线判题,建议把代码里的中文提示信息改成英文或无提示,只输出题目要求的内容。很多时候本地运行正常,但提交到系统就显示“答案错误”,原因就是你把 print("猜想是:", guess) 这类调试信息也输出出来了,结果最后的结果里混入了多余内容。提交前检查一下是否有调试用的 print,是每个初学者都应该养成的习惯。
这一章过去之后,后面的函数、组合数据类型、文件处理,本质上都是这些基础控制结构的组合升级。你不需要在第4章追求把所有题目都背到滚瓜烂熟,但一定得把“怎么拆解问题、怎么控制流程、怎么调试错误”这三件事练成肌肉记忆。等再用到这些语句时,你的注意力就能放到更高层的业务逻辑上,而不是天天纠结为什么循环又多跑了一次。