跳到主要内容

4.4 综合问题的解决

知识点​

1.什么是"综合问题"​

综合问题:需要多种知识、多种算法、多个模块配合才能解决的问题,特征是:

  • 需求不够明确,要先"想清楚要做什么";
  • 涉及多步处理(输入 → 多轮加工 → 输出);
  • 需要组织数据(用列表、字典等结构);
  • 要保证正确(必须测试)。

与单点问题的区别:

单点问题(4.2/4.3)综合问题(4.4)
目标实现一个算法完成一个可用的程序
代码量十几行几十到几百行
关键能力算法思想分析、分解、组织、测试
典型例子求最大公约数成绩管理系统

2.综合问题解决的完整流程(七步,必背)​

① 需求分析 → ② 功能分解 → ③ 数据设计 → ④ 算法设计
→ ⑤ 编码实现 → ⑥ 测试 → ⑦ 优化与文档
步骤做什么产出
① 需求分析明确输入什么、输出什么、有什么限制需求清单(用自然语言写清"要实现哪几件事")
② 功能分解把大任务拆成若干独立小功能功能结构图(模块划分)
③ 数据设计决定用什么结构存数据(列表?字典?二维列表?)数据模型(字段与关系)
④ 算法设计为每个功能设计步骤(可用流程图或伪代码)算法描述
⑤ 编码实现把算法翻译成程序,按模块逐个写、逐个验可运行代码
⑥ 测试用多组数据验证是否正确、健壮测试记录(用例 + 结果)
⑦ 优化与文档改进效率与可读性,写说明文档最终程序 + 使用说明

最容易被跳过、也最致命的两步:① 需求分析 和 ⑥ 测试。

一句话:"想清楚再写,写完必测"。

3.需求分析:先把问题说清楚​

要回答四个问题:

  1. 输入是什么?(几个?什么类型?范围?)
  2. 输出是什么?(格式?精度?)
  3. 要完成哪些功能?(一条条列出来)
  4. 有什么限制?(如"不许用内置排序函数")

示例(成绩管理):

项目内容
输入若干个学生的姓名 + 成绩(人数由用户输入)
输出总分、平均分、最高分、最低分、按成绩降序的名单
功能① 录入 ② 统计 ③ 排序 ④ 查询 ⑤ 输出报表
限制成绩范围 0~100,非法输入要提示

需求分析的价值:需求不清 → 代码返工。这是初学者最大的时间浪费来源。

4.功能分解:自顶向下、模块化​

(1)自顶向下(Top-Down)​

思路:从总目标出发,逐层向下拆分,直到每一层都是"能直接写代码的小功能"。

示例结构图(成绩管理):

成绩管理
├── 录入数据 → input_scores()
├── 统计计算 → calc_stats()
│ ├── 总分
│ ├── 平均分
│ └── 最高/最低分
├── 排序输出 → sort_and_print()
└── 查询学生 → find_student()

(2)模块化​

模块:把功能单一、相对独立的代码封装成函数(或文件)。

模块化的四条好处:

  1. 便于分工——每人写一个函数;
  2. 便于复用——统计函数下次还能用;
  3. 便于调试——出问题只需查一个模块;
  4. 便于维护——改一个功能不动其他部分。

模块划分的原则:高内聚、低耦合——模块内部联系紧密(只做一件事),模块之间依赖少(通过参数与返回值交互,少用全局变量)。

判断标准:能用一句话说清这个函数做什么,且不超过 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()

这段代码体现了四个要点:

  1. 每个函数只做一件事;
  2. 函数之间只通过参数与返回值交互(没有全局变量);
  3. 有文档字符串说明用途;
  4. 有 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)测试记录表(好习惯)​

用例编号输入预期输出实际输出是否通过
185及格及格✓
259不及格不及格✓
360及格不及格✗ 发现 bug
4-5提示"成绩非法"程序崩溃✗ 待修

价值:测试表就是"证据"——改完代码重跑一遍,能确认"改了这里没坏那里"(回归测试)。

8.调试:出了错怎么办​

调试(Debug):定位并修正错误的过程。

(1)三类错误​

类型表现例子怎么发现
语法错误程序根本运行不了缺冒号、中文标点、缩进错解释器直接报错,看行号
运行时错误运行中途崩溃除零、下标越界、类型不匹配看异常类型与堆栈信息
逻辑错误能运行,但结果不对> 写成 >=、循环次数差 1最难查,要靠测试与中间输出

逻辑错误是重点:程序不报错,但答案是错的——这也是"运行通过 ≠ 结果正确"的原因。

(2)五种调试手段​

手段做法适用
看报错信息读异常类型 + 行号 + 箭头位置语法/运行时错误
print 打点在关键位置输出中间变量的值逻辑错误首选
缩小范围注释掉一半代码,看错误是否还在(二分定位)大程序找错
断点调试在 IDE 中设断点,单步执行、查看变量复杂逻辑
手工推演用一小段数据自己在纸上走一遍循环/递归错误

断点调试的三个操作(以 IDLE/PyCharm 为例):设断点 → 单步执行(Step)→ 查看变量值。

(3)调试的心态​

错误不是"失败",而是"信息"——报错信息其实在告诉你哪里错了。学会读报错是编程能力的分水岭。

常见误区:一报错就乱改代码、删掉报错的行、凭感觉加 print——应先读懂提示,再定位,再修改,再验证。

9.优化与文档​

(1)优化的三个层次​

层次做什么示例
正确性优化修 bug、补边界处理输入 abc 不崩溃
可读性优化改命名、加注释、拆函数a1 → total_score
效率优化换算法、减循环层数冒泡排序换 sorted()

顺序不能颠倒:先对、再清、最后快。

过早优化是万恶之源——在程序还不正确时优化效率,往往白费功夫。

(2)代码规范(评分与工程都看)​

  1. 变量与函数命名见名知义(avg_score 而不是 a);
  2. 每个函数有文档字符串说明用途、参数、返回值;
  3. 关键逻辑有注释(说明"为什么",不是"是什么");
  4. 缩进统一 4 空格,一行不超过 79 字符(PEP 8 建议);
  5. 函数短小(一个函数只做一件事)。

(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 打点找出来,体验"程序不报错但结果是错的"。
  • 强调"运行通过 ≠ 正确",这是学生最普遍的误解,也是综合题失分的主要原因。