4.4 综合问题的解决
知识点
1.什么是"综合问题"
综合问题:需要多种知识、多种算法、多个模块配合才能解决的问题,特征是:
- 需求不够明确,要先"想清楚要做什么";
- 涉及多步处理(输入 → 多轮加工 → 输出);
- 需要组织数据(用列表、字典等结构);
- 要保证正确(必须测试)。
与单点问题的区别:
| 单点问题(4.2/4.3) | 综合问题(4.4) | |
|---|---|---|
| 目标 | 实现一个算法 | 完成一个可用的程序 |
| 代码量 | 十几行 | 几十到几百行 |
| 关键能力 | 算法思想 | 分析、分解、组织、测试 |
| 典型例子 | 求最大公约数 | 成绩管理系统 |
2.综合问题解决的完整流程(七步,必背)
① 需求分析 → ② 功能分解 → ③ 数据设计 → ④ 算法设计
→ ⑤ 编码实现 → ⑥ 测试 → ⑦ 优化与文档
| 步骤 | 做什么 | 产出 |
|---|---|---|
| ① 需求分析 | 明确输入什么、输出什么、有什么限制 | 需求清单(用自然语言写清"要实现哪几件事") |
| ② 功能分解 | 把大任务拆成若干独立小功能 | 功能结构图(模块划分) |
| ③ 数据设计 | 决定用什么结构存数据(列表?字典?二维列表?) | 数据模型(字段与关系) |
| ④ 算法设计 | 为每个功能设计步骤(可用流程图或伪代码) | 算法描述 |
| ⑤ 编码实现 | 把算法翻译成程序,按模块逐个写、逐个验 | 可运行代码 |
| ⑥ 测试 | 用多组数据验证是否正确、健壮 | 测试记录(用例 + 结果) |
| ⑦ 优化与文档 | 改进效率与可读性,写说明文档 | 最终程序 + 使用说明 |
最容易被跳过、也最致命的两步:① 需求分析 和 ⑥ 测试。
一句话:"想清楚再写,写完必测"。
3.需求分析:先把问题说清楚
要回答四个问题:
- 输入是什么?(几个?什么类型?范围?)
- 输出是什么?(格式?精度?)
- 要完成哪些功能?(一条条列出来)
- 有什么限制?(如"不许用内置排序函数")
示例(成绩管理):
| 项目 | 内容 |
|---|---|
| 输入 | 若干个学生的姓名 + 成绩(人数由用户输入) |
| 输出 | 总分、平均分、最高分、最低分、按成绩降序的名单 |
| 功能 | ① 录入 ② 统计 ③ 排序 ④ 查询 ⑤ 输出报表 |
| 限制 | 成绩范围 0~100,非法输入要提示 |
需求分析的价值:需求不清 → 代码返工。这是初学者最大的时间浪费来源。
4.功能分解:自顶向下、模块化
(1)自顶向下(Top-Down)
思路:从总目标出发,逐层向下拆分,直到每一层都是"能直接写代码的小功能"。
示例结构图(成绩管理):
成绩管理
├── 录入数据 → input_scores()
├── 统计计算 → calc_stats()
│ ├── 总分
│ ├── 平均分
│ └── 最高/最低分
├── 排序输出 → sort_and_print()
└── 查询学生 → find_student()
(2)模块化
模块:把功能单一、相对独立的代码封装成函数(或文件)。
模块化的四条好处:
- 便于分工——每人写一个函数;
- 便于复用——统计函数下次还能用;
- 便于调试——出问题只需查一个模块;
- 便于维护——改一个功能不动其他部分。
模块划分的原则:高内聚、低耦合——模块内部联系紧密(只做一件事),模块之间依赖少(通过参数与返回值交互,少用全局变量)。
判断标准:能用一句话说清这个函数做什么,且不超过 30~50 行。
5.数据设计:先想好"数据放哪"
| 需求 | 推荐结构 | 理由 |
|---|---|---|
| 一串同类型数据(成绩列表) | 列表 | 可遍历、可排序、可求和 |
| 每条记录有多个字段(姓名+成绩) | 列表套字典/二维列表 | [{"name":"张三","score":92}, …] |
| 按键快速查找(按姓名查成绩) | 字典 | {"张三": 92},查找 O(1) |
| 需要去重 | 集合 | 天然不重复 |
| 固定的少量字段 | 元组 | 不可变,安全 |
示例(成绩记录):
students = [
{"name": "张三", "score": 92},
{"name": "李四", "score": 78},
{"name": "王五", "score": 85},
]
为什么用"列表套字典":列表保证顺序与可遍历,字典让每个学生的字段有名字(s["score"] 比 s[1] 可读)。
数据设计的三问:存什么字段?用什么结构?后续要按什么方式访问?
6.算法设计与编码
(1)算法设计
对每个模块,写出输入 → 处理 → 输出,选择合适策略(见 4.2/4.3):
| 模块 | 算法策略 |
|---|---|
| 求总分 | 累加(循环) |
| 求平均 | 总分 ÷ 人数 |
| 求最高分 | 打擂台法(先取第一个,逐个比较) |
| 排序 | 冒泡/选择或 sorted() |
| 按姓名查找 | 顺序查找(无序) |
(2)编码顺序
推荐"自下而上"逐个实现并测试:先写最简单的函数(如求平均),测通再写下一个,最后写主流程。
反例:一次性写 200 行再运行——一运行十几个错误,根本无从下手。
(3)综合案例:成绩管理(完整结构)
# 4-18 成绩管理(模块化结构)
def input_scores():
"""录入成绩,返回字典列表"""
students = []
n = int(input("学生人数:"))
for i in range(n):
name = input(f"第{i+1}个学生姓名:")
score = float(input(f"{name}的成绩:"))
students.append({"name": name, "score": score})
return students
def calc_stats(students):
"""统计总分、平均分、最高分、最低分"""
scores = [s["score"] for s in students] # 列表推导式取出所有成绩
return {
"total": sum(scores),
"avg": sum(scores) / len(scores),
"max": max(scores),
"min": min(scores),
}
def sort_and_print(students):
"""按成绩降序输出"""
ordered = sorted(students, key=lambda s: s["score"], reverse=True)
print("成绩排名:")
for i, s in enumerate(ordered, 1):
print(f"{i}. {s['name']} {s['score']}")
def find_student(students, name):
"""按姓名查找,返回成绩或 None"""
for s in students:
if s["name"] == name:
return s["score"]
return None
# 主流程
def main():
students = input_scores()
stats = calc_stats(students)
print(f"总分 {stats['total']},平均 {stats['avg']:.2f},"
f"最高 {stats['max']},最低 {stats['min']}")
sort_and_print(students)
who = input("要查询的学生姓名:")
score = find_student(students, who)
print(f"{who} 的成绩是 {score}" if score is not None else "未找到该学生")
main()
这段代码体现了四个要点:
- 每个函数只做一件事;
- 函数之间只通过参数与返回值交互(没有全局变量);
- 有文档字符串说明用途;
- 有
main()主流程,一眼看清整体顺序。
补充说明:sorted(key=lambda …) 中的 lambda 是匿名函数,作用是"告诉排序按哪个字段比"。若教材未涉及,可改用 key=lambda 的等价写法——先按成绩构造 (成绩, 姓名) 元组再排序,或直接用冒泡排序手写。
(4)综合案例:猜数字游戏(结构最简)
# 4-19 猜数字(1~100,最多 7 次)
import random
def play():
answer = random.randint(1, 100)
for i in range(1, 8):
guess = int(input(f"第{i}次猜(1~100):"))
if guess == answer:
print(f"猜对了!用了 {i} 次")
return
elif guess > answer:
print("大了")
else:
print("小了")
print(f"机会用完,答案是 {answer}")
play()
要点:用 for 限制次数(对应二分法的 7 次),用 return 提前结束,逻辑清晰。
(5)综合案例:文本统计(多步骤处理)
# 4-20 统计一段文本的字符数、单词数、最长单词
text = input("请输入一段英文:")
words = text.split() # 按空白切分成单词列表
print("字符数:", len(text))
print("单词数:", len(words))
if words:
longest = max(words, key=len) # 取长度最大的单词
print("最长单词:", longest, f"({len(longest)} 个字母)")
要点:split() 是文本处理的入口——先切成列表,再统计/比较。
7.测试:怎么证明"程序是对的"
(1)测试的基本观念
Dijkstra 的名言:"测试只能证明程序有错,不能证明程序无错。"
所以要做的是:用尽量少的用例,覆盖尽量多的情况,尤其是最容易出错的地方。
(2)测试用例的设计(四类数据,必背)
| 类型 | 含义 | 举例(成绩 0~100) |
|---|---|---|
| 正常数据 | 典型合法输入 | 85、92、60 |
| 边界数据 | 边界上的值 | 0、100、59、60(及格线) |
| 异常数据 | 非法输入 | -5、120、abc、空输入 |
| 特殊数据 | 极端或特殊情况 | 全部同分、只有 1 人、0 人 |
边界值为什么最重要:绝大多数 bug 都藏在边界上(>= 写成 >、range 少一位、数组越界)。
(3)黑盒测试与白盒测试
| 类型 | 关注点 | 是否看代码 | 适用阶段 |
|---|---|---|---|
| 黑盒测试 | 只关心输入与输出是否符合预期 | 不看 | 功能验证、验收测试 |
| 白盒测试 | 关心内部逻辑与执行路径是否都被覆盖 | 看代码 | 单元测试、逻辑检查 |
通俗理解:黑盒 = 只按开关看灯亮不亮;白盒 = 拆开看线路通不通。
(4)测试记录表(好习惯)
| 用例编号 | 输入 | 预期输出 | 实际输出 | 是否通过 |
|---|---|---|---|---|
| 1 | 85 | 及格 | 及格 | ✓ |
| 2 | 59 | 不及格 | 不及格 | ✓ |
| 3 | 60 | 及格 | 不及格 | ✗ 发现 bug |
| 4 | -5 | 提示"成绩非法" | 程序崩溃 | ✗ 待修 |
价值:测试表就是"证据"——改完代码重跑一遍,能确认"改了这里没坏那里"(回归测试)。
8.调试:出了错怎么办
调试(Debug):定位并修正错误的过程。
(1)三类错误
| 类型 | 表现 | 例子 | 怎么发现 |
|---|---|---|---|
| 语法错误 | 程序根本运行不了 | 缺冒号、中文标点、缩进错 | 解释器直接报错,看行号 |
| 运行时错误 | 运行中途崩溃 | 除零、下标越界、类型不匹配 | 看异常类型与堆栈信息 |
| 逻辑错误 | 能运行,但结果不对 | > 写成 >=、循环次数差 1 | 最难查,要靠测试与中间输出 |
逻辑错误是重点:程序不报错,但答案是错的——这也是"运行通过 ≠ 结果正确"的原因。
(2)五种调试手段
| 手段 | 做法 | 适用 |
|---|---|---|
| 看报错信息 | 读异常类型 + 行号 + 箭头位置 | 语法/运行时错误 |
| print 打点 | 在关键位置输出中间变量的值 | 逻辑错误首选 |
| 缩小范围 | 注释掉一半代码,看错误是否还在(二分定位) | 大程序找错 |
| 断点调试 | 在 IDE 中设断点,单步执行、查看变量 | 复杂逻辑 |
| 手工推演 | 用一小段数据自己在纸上走一遍 | 循环/递归错误 |
断点调试的三个操作(以 IDLE/PyCharm 为例):设断点 → 单步执行(Step)→ 查看变量值。
(3)调试的心态
错误不是"失败",而是"信息"——报错信息其实在告诉你哪里错了。学会读报错是编程能力的分水岭。
常见误区:一报错就乱改代码、删掉报错的行、凭感觉加 print——应先读懂提示,再定位,再修改,再验证。
9.优化与文档
(1)优化的三个层次
| 层次 | 做什么 | 示例 |
|---|---|---|
| 正确性优化 | 修 bug、补边界处理 | 输入 abc 不崩溃 |
| 可读性优化 | 改命名、加注释、拆函数 | a1 → total_score |
| 效率优化 | 换算法、减循环层数 | 冒泡排序换 sorted() |
顺序不能颠倒:先对、再清、最后快。
过早优化是万恶之源——在程序还不正确时优化效率,往往白费功夫。
(2)代码规范(评分与工程都看)
- 变量与函数命名见名知义(
avg_score而不是a); - 每个函数有文档字符串说明用途、参数、返回值;
- 关键逻辑有注释(说明"为什么",不是"是什么");
- 缩进统一 4 空格,一行不超过 79 字符(PEP 8 建议);
- 函数短小(一个函数只做一件事)。
(3)文档的三种类型
| 文档 | 内容 | 读者 |
|---|---|---|
| 需求文档 | 要做什么 | 用户、教师 |
| 设计文档 | 模块划分、数据结构、算法说明、流程图 | 开发者 |
| 使用说明 | 怎么运行、怎么输入、输出含义 | 使用者 |
最低要求:程序开头用注释写清 程序名、功能、作者、日期、输入输出说明。
10.速查表
| 序号 | 考点 | 结论 |
|---|---|---|
| 1 | 综合问题七步 | 需求分析→功能分解→数据设计→算法设计→编码→测试→优化文档 |
| 2 | 最易被忽略的两步 | 需求分析、测试 |
| 3 | 设计方法 | 自顶向下、逐步细化 |
| 4 | 模块化原则 | 高内聚、低耦合 |
| 5 | 模块化好处 | 分工、复用、调试、维护 |
| 6 | 函数划分标准 | 一句话说清、30~50 行内 |
| 7 | 存多条记录 | 列表套字典 |
| 8 | 按键快速查找 | 字典 |
| 9 | 编码推荐顺序 | 自下而上,逐个测通 |
| 10 | 测试四类数据 | 正常、边界、异常、特殊 |
| 11 | 最容易出 bug 的地方 | 边界 |
| 12 | 黑盒测试 | 只看输入输出,不看代码 |
| 13 | 白盒测试 | 看内部逻辑与路径 |
| 14 | 回归测试 | 改完代码后重跑原有测试 |
| 15 | 三类错误 | 语法、运行时、逻辑 |
| 16 | 最难查的错误 | 逻辑错误(能运行但结果错) |
| 17 | 逻辑错误首选手段 | print 打点 |
| 18 | 大程序定位错误 | 注释一半,二分缩小范围 |
| 19 | 断点调试三步 | 设断点→单步→看变量 |
| 20 | "测试能证明程序无错" | 错,只能证明有错 |
| 21 | 优化顺序 | 正确 → 可读 → 效率 |
| 22 | 过早优化 | 万恶之源 |
| 23 | 注释应写 | 为什么(不是"是什么") |
| 24 | 三种文档 | 需求、设计、使用说明 |
11.课堂提示(讲课参考)
- 用一个"需求含糊"的题目开场:只说"做个成绩统计程序",让学生分组追问细节(几个人?要不要排名?分数范围?),体会需求分析的必要性。
- 先给"一坨 150 行的代码"让学生改需求(加一个"查最低分"),改不动;再给模块化版本,加一个函数就行——模块化的价值用对比呈现。
- 测试用例让学生自己出:把"成绩 0~100"的程序给他们,要求"找出至少 3 个 bug",学生自然会想到 59、60、-5、abc 这些边界——边界意识自己想出来才牢。
- 故意埋一个逻辑错误(
>=写成>),让学生用 print 打点找出来,体验"程序不报错但结果是错的"。 - 强调"运行通过 ≠ 正确",这是学生最普遍的误解,也是综合题失分的主要原因。