CS1602计算导论
Lab 5Part 2 数据的组织AI Level 0

列表操作与陷阱排查

A 段六题练列表的增删改查、切片与拷贝,把讲义里的三个经典陷阱亲手踩一遍;B 段用一份成绩单从解析做到排名、再做到脏数据,最后看清列表查找的代价;C 段学 git 的本地用法。

截止:小作业 2 · 10 月 27 日 周二 23:59
本页目录

本次目标

  • 熟练使用列表的基本操作与切片
  • 亲手复现并修正三个经典陷阱:[[0]*n]*m、浅拷贝、边遍历边删除
  • 养成用 id() 验证猜想的习惯
  • 用嵌套列表完整处理一份真实形态的数据,并发现它的局限
  • 学会在本地用 git 记录自己的修改

A 段 · 必做

5-1 成绩统计(热身)

给定一个成绩列表,输出人数、平均分、最高分、最低分和及格率。平均分保留两位小数。

输入:[87, 92, 45, 78, 55, 95]
输出:
人数 6
平均 75.33
最高 95
最低 45
及格率 66.67%

5-2 手写 max

不使用 max(),实现 my_max(nums: list[int]) -> int | None 返回列表中的最大值。空列表返回 None。

5-3 有序去重

实现 dedup(nums: list[int]) -> list[int],去掉重复元素并保持原有顺序。

输入:[3, 1, 3, 5, 1, 7]
输出:[3, 1, 5, 7]

不能用 set()——它会打乱顺序。

5-4 矩阵转置

实现 transpose(matrix: list[list[int]]) -> list[list[int]] 返回矩阵的转置。

输入:[[1, 2, 3], [4, 5, 6]]
输出:[[1, 4], [2, 5], [3, 6]]

5-5 找出并修正 Bug

下面的代码想把列表里所有偶数翻倍,但会陷入死循环。找出原因,写出修正版本, 并在提交说明里用一两句话解释原因。

a = [1, 2, 3, 4, 5, 6]
for i in range(len(a)):
    if a[i] % 2 == 0:
        a.insert(i, a[i] * 2)

5-6 拷贝实验

写一段代码,用 id() 验证以下三件事,并把输出贴在提交说明里:

  1. b = a 之后,a 和 b 是同一个对象
  2. b = a[:] 之后,a 和 b 是不同对象
  3. 但对嵌套列表,b = a[:] 之后 a[0] 和 b[0] 仍然是同一个对象

B 段 · 深入:一份成绩单,从头处理到尾

A 段的六道题互不相干。这一段只有一个问题——一份成绩单——但要处理四遍: 先读进来,再排出名次,再面对脏数据,最后看清这套做法在多大规模上会失效。

数据是三十行文本,每行是「学号 姓名 小测1 小测2 小测3」:

RAW = [
    "2026001 zhangsan 91 57 59",
    "2026002 lisi 70 81 55",
    "2026003 wangwu 43 65 66",
    "2026004 zhaoliu - 78 90",
    "2026005 qianqi 90 90 61",
    "2026006 sunba 73 91 73",
    "2026007 zhoujiu 60 53 75",
    "2026008 wushi 105 60 70",
    "2026008 wushi 60 70 80",
    "2026009 zhengyi 60 61 69",
    "2026010 chener 57 48 70",
    "2026011 chusan 59 99 46",
    "2026002 lisi 10 10 10",
    "2026012 weisi 43 59 89",
    "2026013 jiangwu 95 96 48",
    "2026014 shenliu 53 73 62",
    "2026015 hanqi 64 73 96",
    "2026016 yangba 59 71 85",
    "2026017 zhujiu 64 41",
    "2026018 qinshi 59 81 93",
    "2026011 chusan 88 88 88",
    "2026019 youyi 97 73 62",
    "   2026020   xuer   70 80 90  ",
    "2026021 hesan 82 90 80",
    "2026022 lvsi 61 55 60",
    "2026023 shiwu 58 x 88",
    "2026024 zhangliu 63 93 95",
    "2026019 youyi 40 40 40",
    "2026025 kongqi 41 100 67",
    "2026026 caoba 99 50 48",
]

三十行里混着几种脏数据,B-3 才处理;B-1 和 B-2 先假定每行都是干净的。

不要用文件读写——那是第 12 讲的内容。把上面这段复制到你的代码里。

B-1 读进来

实现 parse_roster(lines: list[str]) -> list[list],把每行文本解析成一条记录:

["2026001", "zhangsan", [87, 92, 45]]

学号保留成字符串(它不是用来算术的),三次小测放进一个列表。

输入:["2026001 zhangsan 87 92 45", "2026002 lisi 78 65 90"]
输出:[["2026001", "zhangsan", [87, 92, 45]],
      ["2026002", "lisi", [78, 65, 90]]]

B-2 排出名次

实现 rank(records: list[list]) -> list[list],按总分从高到低排序, 总分相同的按学号从小到大。返回:

[[1, "2026003", "wangwu", 274],
 [2, "2026002", "lisi", 233],
 [3, "2026001", "zhangsan", 224]]

名次从 1 开始连续编号,不处理并列。

B-3 脏数据

真实的数据不会这么干净。完整的 RAW 里混着这些情况:

情况RAW 里的例子怎么处理
缺考2026004 zhaoliu - 78 90- 按 0 分计入,这条记录保留
列数不对2026017 zhujiu 64 41(少一次小测)整条丢掉
分数非法2026008 wushi 105 60 70、2026023 shiwu 58 x 88不是整数或不在 0–100,整条丢掉
学号重复2026002、2026011、2026019 各出现两次保留先出现的那条
多余空格 2026020 xuer 70 80 90 正常保留

实现 clean(lines: list[str]) -> list[list],返回清洗后的记录, 形状和 parse_roster 一样。按上表从上到下的顺序检查: 先看列数,再看分数是否合法,最后在活下来的记录里去重。

同时在提交说明里写清楚:丢掉了几条、分别因为什么。

B-4 这套做法在什么规模上失效

现在做几件查询的事,都用列表来做:

  1. 写 find(records, sid),按学号找出一条记录。
  2. 在 find 里加一个计数器,数清楚为了找到目标,一共比较了多少次。 分别查排在第 1 条、第 15 条、第 30 条的学号,各记下比较次数。
  3. 写 has_duplicate(records),判断有没有重复学号——只用列表,不用 set。 同样数清楚比较了多少次。

然后回答:

  1. find 的比较次数和记录条数是什么关系?
  2. has_duplicate 呢?它比 find 严重在哪里?
  3. 这门课有 75 人,这套做法完全够用。如果是全校三万人的成绩库, 每天要查几万次,会发生什么?
  4. 你希望有一种什么样的容器,能让「按学号查一条记录」这件事不用挨个比?

C 段 · 基本功:git 入门

到这一周,你写的代码已经有分量了:B 段那几个函数改错一次就可能回不去。 git 就是用来解决这件事的——它替你记住每一个版本,让你随时能退回去。

这周只在自己电脑上用。下周学怎么推到 GitHub。

第一次使用要先配置身份

git config --global user.name "你的名字"
git config --global user.email "你的邮箱"

每次提交都会记下是谁提交的,所以 git 要先知道你是谁。这一步只做一次。

六个命令

命令作用
git init在当前目录建一个仓库。只在项目开始时做一次
git status当前有哪些改动、哪些已经暂存。不确定的时候就执行一次
git add 文件把改动放进暂存区,表示「这些我要提交」
git commit -m "说明"把暂存区里的改动记成一个版本
git log --oneline看提交历史,一行一条
git diff看还没暂存的改动;git diff --staged 看已暂存的

C-1 走一遍

在本周实验的目录里操作,把每一步的命令和输出记进提交说明:

  1. git init,然后 git status 看一眼——它说了什么?
  2. 建一个 .gitignore,至少写上 __pycache__/
  3. git add 你的实验代码,再 git status,看输出有什么变化
  4. git commit -m "lab05 A 段" 提交第一版
  5. 随便改一个函数,git diff 看看它告诉你什么。注意开头的 - 和 + 分别是什么意思
  6. git add 之后再 git diff——为什么这次什么都没有了?换 git diff --staged 试试
  7. 提交第二版,git log --oneline 看历史

C-2 想一想

  1. 既然最终都要 commit,为什么 git 还要多一个「暂存区」? 什么场景下你会只想提交一部分改动?
  2. git log 里的提交信息该怎么写?比较这两条,哪条在三个月后对你更有用: "改了一下" 和 "B-2 排序改用 [-总分, 学号],修掉并列时顺序不定的问题"

提交

A 段六题在评测平台上提交,每题独立判分。 B 段的 parse_roster、rank、clean 也在平台上判;B-4 和 C 段的回答写在提交说明里。

提交前自查

  • 每个函数都有类型提示
  • 5-4 的错误输出记在提交说明里了
  • 5-5 的死循环原因解释了
  • 5-6 的三段 id() 输出贴上了
  • B-3 说明了丢掉几条、为什么,并自己举了一个换顺序会出错的例子
  • B-4 的四个问题都回答了
  • C 段的命令输出和两个问题都写了