1. 从一道练习题说起:为什么"程序设计基础"的练习题值得单独拎出来讲
很多人看到"程序设计基础练习题"这几个字,第一反应是——这不就是课后作业吗,有什么好聊的。但我带了这么多届刚入门的学生,也帮不少转行的朋友做过一对一辅导,越来越觉得:程序设计基础的练习题,是整个编程学习路径里性价比最高、也最容易被浪费掉的一环。
它性价比高,是因为这些题目几乎覆盖了编程最核心的思维模型:变量、分支、循环、数组、函数、递归。你后面学再花哨的框架、再复杂的系统,底层跑的还是这些东西。它容易被浪费,是因为大部分人做练习题的方式就是"能跑通就行"——输出对了就翻篇,从不回头看自己的代码是不是写复杂了、有没有边界漏洞、换个条件还能不能撑住。
这篇内容我想干的事很明确:把"程序设计基础练习题"当成一个正经的学习项目来拆解。不是给你抄答案,而是把每类题目背后的出题意图、常见解法、踩坑点和进阶思路讲透。适合刚学编程的在校生、准备转行的自学者,也适合已经工作但想回头补基础的朋友。你把这套思路吃透,再去做任何一套练习题,效率会完全不一样。
2. 练习题到底在练什么:先搞懂出题人的意图
2.1 三类题目的本质区别
程序设计基础的练习题,看着五花八门,其实按考察目标可以分成三大类。搞清楚你手上这道题属于哪一类,比急着敲代码重要得多。
第一类是语法熟练度题。比如"输入两个数,输出它们的和"、"判断一个年份是不是闰年"。这类题目的目的不是考你算法,而是让你把输入输出、变量声明、条件判断这些基本动作练到不用想就能写出来。很多人瞧不上这类题,觉得太简单,但恰恰是这类题决定了你后面写代码的"手感"。
第二类是逻辑建模题。比如"打印九九乘法表"、"求1到100之间所有偶数的和"、"判断一个数是不是素数"。这类题开始要求你把一个自然语言的描述,翻译成计算机能执行的步骤。难点不在语法,而在于你能不能把"怎么做"这件事拆成一步步明确的指令。
第三类是综合应用题。比如"输入一串数字,排序后输出"、"统计一段文字里每个字符出现的次数"。这类题往往需要组合多个知识点,是检验你前面学得扎不扎实的试金石。
提示:拿到一道题,先花30秒判断它属于哪一类。语法题追求"快而准",逻辑题追求"拆得清",综合题追求"组合稳"。目标不同,练习的侧重点也不同。
2.2 为什么"能跑通"远远不够
我见过太多这样的代码:题目要求求最大值,他写了个循环,能跑出正确结果,但代码里变量命名是a、b、c、temp,逻辑嵌套了三层if,还硬编码了一个数组长度。这种代码在练习题阶段"过关"了,但一旦题目条件变一下——比如数组长度改成动态输入——立刻就崩。
真正有效的练习,是在"能跑通"之后多问自己三个问题:
- 边界情况考虑了吗?输入为空、输入只有一个元素、输入有重复值,这些情况我的代码还成立吗?
- 有没有更简洁的写法?我用了十行,能不能用五行?不是为了炫技,而是简洁往往意味着逻辑更清晰。
- 这段代码换个场景还能用吗?我写的这个求最大值的逻辑,能不能抽成一个函数,下次直接调用?
这三个问题,就是普通练习者和高手之间的分水岭。练习题的价值不在于题目本身,而在于你借它训练出的思维习惯。
2.3 一套练习题的正确打开方式
我一般建议按这个节奏来刷题,实测下来效率最高:
- 先读题,不动手。把题目要求读两遍,在纸上或脑子里把输入、输出、中间过程理一遍。
- 手写伪代码。不用管语法,用中文或简单符号把步骤写出来。这一步能帮你发现逻辑漏洞。
- 翻译成代码。把伪代码逐句翻译成目标语言,遇到不会的语法现查现学。
- 测试三组数据。一组正常数据、一组边界数据、一组"刁钻"数据(比如负数、零、空值)。
- 回头优化。看看能不能简化、能不能抽函数、命名能不能更清楚。
这套流程看着慢,但坚持两三周之后,你会发现自己的代码质量和调试速度都有明显提升。因为它逼着你在动手之前先动脑,而不是边写边改、越改越乱。
3. 核心知识点的练习题拆解与实操要点
3.1 变量与输入输出:别小看最基础的一步
变量和输入输出是所有练习题的起点,但这里恰恰藏着不少新手坑。我拿一道最经典的题举例:输入两个整数,交换它们的值并输出。
这道题看着简单,但它其实在考你对"变量是存储空间"这个概念的理解。新手最常见的错误写法是这样的:
a = 10 b = 20 a = b b = a print(a, b) # 输出 20 20,错了问题出在哪?a = b执行后,a原来的值10就被覆盖了,等到b = a时,a已经是20了。正确的做法是引入一个临时变量:
a = 10 b = 20 temp = a a = b b = temp print(a, b) # 输出 20 10在Python里还有个更优雅的写法,利用元组解包:
a, b = b, a这一行就完成了交换,背后是Python先把右边打包成一个元组,再解包赋值给左边。这个技巧在实际开发里很常用,值得记住。
注意:交换变量这道题,在C语言里必须用临时变量,在Python里可以用元组解包。不同语言的特性不一样,做练习题时要有意识地区分"通用逻辑"和"语言特性"。
实操心得:做输入输出类题目时,一定要养成先验证输入、再处理逻辑的习惯。比如题目说"输入一个正整数",你就应该想到:如果用户输入了负数或者零怎么办?虽然练习题里测试数据通常是规范的,但提前养成防御性思维,对你后面写真实程序帮助极大。
3.2 分支结构:把"如果"想全了
分支结构的练习题,核心考察的是逻辑完备性。最典型的题目是"判断闰年"。闰年的规则是:能被4整除但不能被100整除,或者能被400整除。很多人第一次写会写成这样:
if year % 4 == 0: print("闰年") else: print("平年")这就漏掉了"能被100整除但不能被400整除的不是闰年"这个条件。正确的写法是:
if (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0): print("闰年") else: print("平年")这道题的价值在于,它训练你把一个复合条件准确地翻译成逻辑表达式。and和or的优先级、括号的使用,都是这里要练的基本功。
再比如"根据成绩输出等级"这类题:90分以上是A,80到89是B,以此类推。新手容易写成嵌套很深的if-else,读起来费劲。更好的写法是用elif把条件拉平:
if score >= 90: grade = 'A' elif score >= 80: grade = 'B' elif score >= 70: grade = 'C' elif score >= 60: grade = 'D' else: grade = 'F'这里有个细节:elif score >= 80不需要再写score < 90,因为如果分数大于等于90,第一个分支就已经拦截了。理解这一点,能帮你写出更简洁的分支逻辑。
提示:写分支结构时,把最严格、最特殊的条件放在最前面,最宽松的条件放在最后。这样能避免条件重叠导致的逻辑错误。
3.3 循环结构:从"重复"里找规律
循环是程序设计基础里最重要、也最容易出问题的部分。练习题里关于循环的题目最多,我挑几个典型类型来讲。
类型一:累加累乘。比如"求1到100的和"、"求n的阶乘"。这类题的核心是初始化累加器。求和时初始值设为0,求积时初始值设为1。这个细节新手经常搞混,求积时把初始值设成0,结果永远是0。
total = 0 for i in range(1, 101): total += i print(total) # 5050 factorial = 1 for i in range(1, n + 1): factorial *= i类型二:嵌套循环。比如"打印九九乘法表"、"打印金字塔图案"。嵌套循环的难点在于理解内外层的关系。外层控制行,内层控制列,这是最常见的模式。
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)是关键,它让内层循环的次数随外层变化,从而打印出三角形的形状。理解"内层循环的边界依赖外层变量"这个模式,你就能解决一大类图形打印题。
类型三:循环加条件判断。比如"求1到100之间所有素数的和"。这类题需要你在循环里嵌套判断,考察的是多个知识点的组合能力。
def is_prime(n): if n < 2: return False for i in range(2, int(n ** 0.5) + 1): if n % i == 0: return False return True total = sum(i for i in range(2, 101) if is_prime(i)) print(total)注意这里判断素数时,循环只到n ** 0.5(即根号n)就够了。为什么?因为如果一个数n有因子,那么必然有一个因子小于等于根号n。这个优化能把时间复杂度从O(n)降到O(根号n),是判断素数题目的标准优化手段。
注意:循环里最容易出的bug是"差一错误"(off-by-one error),也就是循环多跑一次或少跑一次。写循环时,先确认边界是包含还是不包含,
range(a, b)在Python里是不包含b的,这一点要记牢。
3.4 数组与字符串:批量数据的处理思维
数组和字符串的练习题,考察的是批量处理数据的能力。最经典的题目包括"求数组最大值最小值"、"数组反转"、"统计字符出现次数"等。
拿"求数组最大值"来说,新手容易犯的错是把初始最大值设成0。如果数组全是负数,结果就错了。正确做法是把初始值设成数组的第一个元素:
def find_max(arr): if not arr: return None max_val = arr[0] for num in arr[1:]: if num > max_val: max_val = num return max_val这里还顺手处理了空数组的情况,返回None。这种防御性写法在实际开发里是必须的。
再比如"统计字符串里每个字符出现的次数",这是字典(哈希表)的经典应用场景:
def count_chars(s): counts = {} for ch in s: counts[ch] = counts.get(ch, 0) + 1 return countscounts.get(ch, 0)这个写法很关键,它的意思是"如果ch在字典里就取它的值,否则返回0"。这样就不用先判断key是否存在,代码更简洁。这个技巧在统计类题目里几乎天天用到。
数组类题目还有一个高频考点是双指针。比如"判断一个字符串是不是回文",用双指针从两端向中间逼近,比反转字符串再比较更高效:
def is_palindrome(s): left, right = 0, len(s) - 1 while left < right: if s[left] != s[right]: return False left += 1 right -= 1 return True双指针的思想在后面学算法时会反复出现,在基础阶段就把它练熟,收益很大。
3.5 函数与递归:把问题拆小的艺术
函数和递归是程序设计基础里最"烧脑"的部分,也是最能拉开差距的地方。函数的核心思想是封装和复用,递归的核心思想是把大问题拆成同类的小问题。
先说函数。练习题里常见的"写一个函数判断素数"、"写一个函数计算阶乘",目的都是让你养成"把一段逻辑封装成函数"的习惯。好的函数应该满足:功能单一、命名清晰、输入输出明确。
def gcd(a, b): """计算两个数的最大公约数""" while b: a, b = b, a % b return a这个求最大公约数的函数用的是辗转相除法,短短几行,逻辑清晰。注意它的命名gcd(greatest common divisor),一看就知道干什么。函数命名这件事,新手往往不重视,但它是代码可读性的第一道关。
再说递归。递归的经典练习题包括"计算阶乘"、"斐波那契数列"、"汉诺塔"等。递归的关键是找到两个东西:递归出口(什么时候停)和递归关系(怎么把问题变小)。
def factorial(n): if n <= 1: # 递归出口 return 1 return n * factorial(n - 1) # 递归关系写递归时,新手最容易犯的错是忘记写递归出口,导致无限递归、栈溢出。另一个常见问题是重复计算,比如朴素的斐波那契递归:
def fib(n): if n <= 1: return n return fib(n - 1) + fib(n - 2)这个写法在n稍微大一点(比如n=40)时就会慢得离谱,因为它把同样的子问题算了很多遍。改进方法是加一个缓存(记忆化):
from functools import lru_cache @lru_cache(maxsize=None) def fib(n): if n <= 1: return n return fib(n - 1) + fib(n - 2)这个优化思路在练习题阶段就值得了解,因为它让你第一次直观感受到"算法效率"这件事的重要性。
提示:递归虽然优雅,但不是所有问题都适合用递归。能用循环解决的简单问题,优先用循环,因为递归有栈溢出的风险,而且调试起来更麻烦。递归更适合处理"天然具有递归结构"的问题,比如树的遍历。
4. 完整实操:从读题到优化的全流程演示
4.1 选一道综合题来走一遍
光讲知识点还是有点散,我拿一道综合题把整个流程串一遍。题目是:输入一行字符串,统计其中每个单词出现的次数,按出现次数从高到低输出。
这道题综合了字符串处理、字典、排序三个知识点,是典型的综合应用题。我们按前面说的五步流程来走。
第一步:读题,理清输入输出。输入是一行字符串,输出是"单词 + 次数"的列表,按次数降序排列。中间过程需要:把字符串拆成单词、统计每个单词的次数、按次数排序。
第二步:手写伪代码。
读取一行字符串 把字符串按空格拆分成单词列表 遍历单词列表,用字典统计每个单词的次数 把字典转换成(单词, 次数)的列表 按次数降序排序 输出结果第三步:翻译成代码。
def word_count(text): words = text.split() counts = {} for word in words: counts[word] = counts.get(word, 0) + 1 sorted_items = sorted(counts.items(), key=lambda x: x[1], reverse=True) return sorted_items text = input("请输入一行文字:") for word, count in word_count(text): print(f"{word}: {count}")第四步:测试三组数据。
- 正常数据:
"hello world hello"→ hello出现2次,world出现1次,排序正确。 - 边界数据:
""(空字符串)→ split后是空列表,counts是空字典,输出为空,不报错。 - 刁钻数据:
"a a a b b c"→ 验证排序是否稳定、次数是否正确。
第五步:回头优化。上面的代码已经比较清晰了,但还能再改进两点:一是用collections.Counter简化统计逻辑,二是处理大小写问题(比如"Hello"和"hello"应该算同一个单词)。
from collections import Counter def word_count(text): words = text.lower().split() counts = Counter(words) return counts.most_common() text = input("请输入一行文字:") for word, count in word_count(text): print(f"{word}: {count}")Counter是Python标准库里的计数器,most_common()直接返回按次数降序排列的结果,代码从十几行缩到了几行。这就是"回头优化"的价值——不是炫技,而是让代码更可靠、更易读。
4.2 参数选择与边界处理的实际考量
在上面这道题里,有几个参数和边界需要特别说明,这也是很多人做练习题时容易忽略的地方。
关于split()的参数。不传参数的split()会按任意空白字符(空格、制表符、换行)分割,并且自动忽略连续空白。如果传了参数,比如split(" "),那么连续两个空格之间会产生一个空字符串。这个区别在处理真实文本时很关键,我一般建议统计单词时用不带参数的split()。
关于排序的稳定性。Python的sorted()是稳定排序,意思是次数相同的单词会保持它们在原字典里的顺序。如果你希望次数相同时按字母顺序排列,可以这样写:
sorted_items = sorted(counts.items(), key=lambda x: (-x[1], x[0]))这里-x[1]表示按次数降序,x[0]表示次数相同时按单词升序。这种"多关键字排序"的技巧在练习题里不常见,但在实际数据处理里非常有用,值得提前掌握。
关于大小写和标点。真实文本里单词可能带标点,比如"hello,"和"hello"应该算同一个。处理方法是先去掉标点:
import string text = text.translate(str.maketrans('', '', string.punctuation))这一行会把所有标点符号删掉。练习题里通常不会要求这么细,但你自己做练习时加上这一步,能训练"考虑真实场景"的思维。
注意:做综合题时,不要一上来就追求完美。先把核心逻辑跑通,再逐步加边界处理。一次性把所有细节都想全,反而容易卡住。
4.3 代码复盘:把练习题的价值榨干
做完一道题,真正的学习才刚刚开始。我习惯做一次"代码复盘",具体看这几个方面:
| 复盘维度 | 检查问题 | 改进方向 |
|---|---|---|
| 正确性 | 所有测试数据都通过了吗 | 补充边界测试 |
| 可读性 | 变量命名清楚吗,逻辑好懂吗 | 重命名、加注释 |
| 简洁性 | 有没有冗余代码 | 用内置函数、简化逻辑 |
| 健壮性 | 异常输入会崩吗 | 加输入校验 |
| 复用性 | 能抽成函数吗 | 封装、参数化 |
这张表我建议你做完每道综合题都过一遍。坚持一段时间,你会发现自己写代码时"下意识"就会考虑这些问题,而不是等复盘时才想起来。
5. 常见问题与排查技巧实录
5.1 新手最容易踩的五个坑
带学生的过程中,我发现有些错误反复出现,几乎每个人都会踩一遍。我把它们整理出来,你对照着检查自己的代码。
坑一:变量未初始化就使用。比如累加时忘了把total设成0,直接total += i,程序报错。解决方法是养成习惯:用变量之前先想清楚它的初始值是什么。
坑二:混淆赋值和比较。在C语言里,if (a = b)是赋值,if (a == b)才是比较。写错了编译器可能不报错,但逻辑全错。Python里虽然不会出现这个问题,但养成"比较用双等号"的习惯很重要。
坑三:循环边界搞错。该循环n次的结果循环了n+1次,或者反过来。排查方法是把循环变量打印出来,看它实际取了哪些值。
坑四:数组越界。访问arr[len(arr)]会报错,因为索引是从0到len(arr)-1。写循环时,range(len(arr))是安全的,range(len(arr)+1)就越界了。
坑五:递归没有出口。递归函数忘了写终止条件,导致无限递归。排查方法是检查每个递归分支最终能不能到达出口。
5.2 调试技巧:让bug自己现形
调试是编程的核心技能,但很多人在练习题阶段没养成好的调试习惯,遇到问题就靠"猜"。我分享几个实用的调试方法。
方法一:打印中间变量。最简单也最有效。在关键位置插入print,看变量的实际值和你预期的是否一致。比如循环里打印循环变量,就能立刻发现边界问题。
方法二:缩小问题规模。如果程序在输入100个数时出错,先试试输入3个数,手动推演一遍,看哪一步开始不对。小规模数据更容易在脑子里跟踪。
方法三:二分定位。如果代码很长,在中间插一个print,看前半部分对不对。对,说明bug在后半部分;不对,说明bug在前半部分。这样能把排查范围快速缩小一半。
方法四:橡皮鸭调试法。把代码逐行讲给一个"橡皮鸭"(或者任何不会回话的东西)听。讲的过程中,你往往会自己发现逻辑漏洞。这个方法听着玄乎,但实测非常有效。
提示:调试时不要一次改很多地方。改一处,测一次,确认这处改对了再改下一处。一次改太多,出了问题你都不知道是哪个改动导致的。
5.3 常见问题速查表
我把练习题阶段最常见的问题和解决方法整理成一张表,方便你遇到问题时快速定位。
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 程序无输出 | 循环没执行、条件永远为假 | 检查循环边界和条件表达式 |
| 结果差1 | 差一错误 | 检查range边界、索引范围 |
| 结果全为0 | 累加器初始值错误 | 求和初始0,求积初始1 |
| 无限循环 | 循环变量没更新 | 检查循环体内是否改变了循环条件 |
| 递归报栈溢出 | 缺少递归出口 | 补上终止条件 |
| 字符串处理乱码 | 编码问题 | 确认输入输出编码一致 |
| 浮点数比较失败 | 精度问题 | 用差值小于某个小量来判断相等 |
这张表建议收藏,遇到问题先扫一眼,能省下不少瞎折腾的时间。
5.4 从练习题到真实项目的思维转变
最后说一个很多人忽略的点:练习题和真实项目的差距在哪。练习题通常是"输入确定、输出确定、边界清晰",而真实项目往往是"需求模糊、数据脏乱、边界未知"。所以做练习题时,要有意识地往真实场景靠。
比如做"统计单词次数"这道题,练习题可能只给你一句干净的英文。但真实场景里,文本可能有中文、有标点、有大小写混杂、有超长文本。你在练习时主动加上这些处理,就是在提前适应真实项目。
再比如做"求数组最大值"这道题,练习题可能保证数组非空。但真实场景里,空数组、null值都可能出现。你主动加上空值判断,就是在培养工程思维。
这种"多想一步"的习惯,是练习题能给你的最大价值。题目本身会过时,但这种思维方式会跟着你一辈子。
我在实际带人的过程中发现,那些进步快的同学,往往不是刷题最多的,而是每道题都愿意多琢磨一层的。他们会问"如果输入变了怎么办"、"这段逻辑能不能复用"、"有没有更通用的写法"。这种习惯一旦养成,后面学任何新技术都会事半功倍。所以别急着刷下一道题,先把手上这道题的价值榨干。