Ran Wei/计算机科学系列/模块 01
English
计算机科学基础 — Ran Wei

模块 01:什么是计算?

把问题变成约定,跟踪搜索,解释为什么正确,并统计工作量;再把这次小计算放入计算机的运行层次中。

约 5 小时4 个时段3 个实验8 道练习8 道自测题

完成后你能够

  • 区分问题规约、算法与程序。
  • 明确搜索约定,涵盖空列表、缺失目标与重复值。
  • 跟踪线性搜索并解释状态变化。
  • 分别解释正确性、终止性与比较次数。
  • 识别抽象与运行程序涉及的层次。

开始之前

无需先修模块或编程经验。实验前阅读简短的 Python 记法介绍。需要浏览器、文本编辑器与 Python 3.11 或更新版本,不使用第三方包。

目录

学习计划

5 小时

四个时段,每个 75 分钟。时间为估计,包含动手实践。练习 6–7 为可选拓展(额外 25 分钟)。勾选时段可在当前浏览器保存进度。

1

计算从一个问题开始

书架上有四本书,你想知道《The Hobbit》在哪里。你可以沿着书架逐本阅读书名,找到后便停下来。这个简单过程已经包含计算的基本要素:信息、明确的问题、一系列步骤和结果。

计算是按照规则转换信息的过程。信息可以描述数字、文字、图像、动作或关系。计算机使用物理机器执行这些规则;人也可以用纸笔完成同一个小规模计算。计算机科学研究如何表示问题、构造并分析求解过程、组织系统,以及理解计算能做什么、不能做什么。

程序设计让我们用机器能够执行的形式表达一个过程。计算机科学还会追问:过程是否回答了正确的问题?是否一定结束?需要多少工作?如何融入更大的系统?即使换一种编程语言,这些问题仍然存在。

把问题说清楚

图书目录是一个有序列表:["Dune", "Foundation", "The Hobbit", "Dune"]。问题是:第一个与目标书名完全相等的项目,其索引是多少?索引从零开始。查询 "The Hobbit" 得到 2;查询 "Dune" 得到 0;查询 "Solaris" 则约定返回 -1,表示不存在。

我们已经做出几项决定:顺序有意义,匹配必须完全相等,允许重复书名,未找到也有明确结果。“找一本书”本身并没有说明这些要求。消除这种歧义是计算工作的起点。

检查理解

统计书的数量是否属于计算?说明输入与输出。

完整解答

是。输入为有限目录,输出为项目数。每读一个项目便把计数器增加一。

2

明确输入、输出与假设

问题规约说明输入与输出之间应满足的关系。算法是实现这一关系的步骤。程序则用具有明确执行规则的编程语言实现这些步骤。同一个问题可以有多种算法,同一个算法也可以有多种程序实现。

组成部分搜索操作的约定
输入一个有限、有序的书名列表,以及一个目标书名。
假设书名是字符串。搜索期间列表不变。使用区分大小写的精确比较。
存在时的输出书名等于目标的最小索引。
不存在时的输出-1,空列表也如此。
副作用搜索不会修改列表。

假设是前置条件:开始执行前必须成立的条件。承诺的输出和输入不变性是后置条件:返回后必须成立的条件。它们共同构成操作的约定,也称契约。后续模块会把这一思想用于数据结构、网络协议和数据库事务。

小心哨兵值

这里的 -1 是哨兵值,用一个约定的特殊值表示不存在。但 Python 允许负数索引:books[-1] 会读取最后一本书。因此,把搜索结果当作索引之前,必须先检查它是否为 -1。以后也可以改为返回 None;那是另一种约定。

用户可能希望“dune”也能匹配“Dune”。这需要新的匹配规则,例如不区分大小写。不能悄悄改变含义,却仍然声称遵守原来的约定。

检查理解

空目录应返回什么?索引 0 是否表示不存在?

完整解答

空目录返回 -1。非空目录中的索引 0 是第一个有效位置。

3

描述算法并跟踪状态

线性搜索按给定顺序检查项目。伪代码表达步骤,而不依赖特定编程语言:

依次遍历从第一个到最后一个位置 i:
    如果位置 i 的书名等于目标:
        返回 i
返回 -1

最后一个返回语句仅在所有项目都检查完后执行。循环内部的返回会立即结束整个搜索。因此,即使书名重复出现,结果仍然是第一个匹配位置。

算法运行时具有状态,即描述当前执行位置所需的信息。本例中变化的状态包括当前位置;如果测量工作量,还包括比较次数。执行轨迹逐步记录这些状态。

位置书名等于 The Hobbit?下一步
0Dune否向后移动
1Foundation否向后移动
2The Hobbit是返回 2

这次执行不会检查第四本书。运行代码之前先预测轨迹,可以帮助你区分“我认为它会做什么”与“它实际做了什么”。

交互演示:逐步执行线性搜索

    进行三次实验:查询 Dune,解释为什么不会访问索引 3;查询 Solaris,统计比较次数;选择空目录,解释为什么无需比较任何书名就能返回 -1。演示把“列表已经耗尽”单独显示为一步;这一步不属于书名比较。

    检查理解

    跟踪查询 Foundation 的过程。检查哪些位置?

    完整解答

    先检查 0,再检查 1。比较两次后返回 1,不检查 2 或 3。

    4

    正确性、终止性与工作量

    例子展示某次具体执行,而推理需要覆盖满足约定的所有输入。后面会学习形式化证明;这里先使用一个简短的推理模式。

    正确性:什么始终成立?

    检查位置 i 之前,前面已经检查的位置都不包含目标。这是一条循环不变式,在循环推进时保持成立。开始检查索引 0 时,前面没有位置,因此成立。如果当前书名不匹配,把它加入已检查的前缀后,该性质仍成立。如果匹配,不变式说明前面没有更早的匹配,因此返回 i 满足约定。如果列表耗尽,说明所有书名都不匹配,返回 -1 也满足约定。

    终止性:什么在推进?

    对长度为 n 的列表,每次未成功匹配的迭代都前进一个位置。尚未检查的项目数减少一,并且不可能无限减少到零以下。找到匹配则会更早结束。因此,一个有限且搜索期间不变的列表能够保证终止。

    工作量:先定义要统计的操作

    我们把一次书名相等性检查作为统计单位。非空列表中,第一个位置就匹配时需要一次比较;最后一个位置匹配或目标不存在时,需要 n 次比较;空列表需要零次比较。项目数翻倍,最坏情况下的比较次数也翻倍。

    这是一个模型,不是实际耗时。比较两个很长的字符串可能需要逐字符检查,因此当书名长度变化时,一次书名比较未必是常数时间。机器、实现方式和数据也会影响秒数。模块 05 将进一步区分这些概念并介绍大 O 表示法。现在要做到的是明确统计单位,并正确计数。

    三个不同的问题

    结果是否符合要求?过程是否会结束?需要多少工作?快速得到的错误答案仍然错误;永不结束的循环也无法交付它承诺的答案。

    检查理解

    为什么空目录也符合不变式推理?

    完整解答

    没有更早的匹配,也没有待检查项目。因为根本没有项目,所以不存在匹配,返回 -1 正确。

    5

    抽象与计算机的层次

    抽象保留任务所需的性质,同时隐藏其他细节。把目录看成有序列表,我们就能推理位置,而不必知道某台机器如何保存字符串。把搜索封装成函数,程序的其他部分就能使用结果,而不必重复搜索步骤。

    抽象必须保留关键性质。若把有序列表换成没有有意义顺序的集合,“第一个索引”就失去含义。若只保留书名并丢弃作者,就无法在不增加信息的情况下查询作者。

    1. 问题与应用用户询问目标书名在目录中的位置。
    2. 算法与程序线性搜索描述步骤;Python 把它们表达为可执行代码。
    3. 语言实现Python 运行时执行列表和字符串上的操作。
    4. 操作系统管理进程、内存,以及对文件和设备的访问。
    5. 硬件CPU 指令对存储在内存中的数据进行操作。
    运行本例程序所涉及层次的简化视图。这里描述的是职责,并不意味着每个操作都要调用所有层次。模块 09–11 将详细介绍它们的边界。

    运行脚本时,CPU 不会直接理解书名或“找到第一个匹配项”这句自然语言。语言实现和机器指令把这些含义连接到物理操作。你可以先学会推理搜索过程,再逐步学习底层机制。

    这种分层也有助于定位故障:需求可能规定了错误的匹配规则;算法可能跳过索引 0;程序可能把 return -1 放进循环;运行环境可能无法打开文件。修改代码之前,先判断哪一层职责出了问题。

    检查理解

    只包含书名的抽象能回答每本书的作者吗?为什么?

    完整解答

    一般不能,因为没有保留作者信息。需要扩展表示,或从其他来源获取。

    6

    常见误解

    误解应当检查什么
    “计算只是算术。”搜索文字和寻找路线同样按照规则转换信息。
    “算法就是一个 Python 文件。”区分步骤本身与特定语言的实现。
    “成功运行一次就证明正确。”检查边界情况,并推理所有有效输入。
    “索引零表示没找到。”零是有效位置。本例用 -1 表示不存在。
    “四本书就一定比较四次。”提前匹配会停止搜索,实际工作量取决于目标。
    “所有错误都出在算法。”分别检查需求、过程、实现和环境。
    7

    准备实验环境

    准备 Python 3.11 或更新版本、文本编辑器和终端。这些实验仅使用 Python 的基本功能,不需要安装第三方包,也不需要下载数据集。下载下面的脚本,或把代码复制到同名文件。在该目录的终端中运行 python lab1_trace.py。Windows 也可使用 py -3 lab1_trace.py;若系统中的命令名是 python3,则使用该命令。

    "Dune" 这样的带引号值是字符串;方括号表示列表。for 重复执行缩进的代码块;enumerate 提供索引和项目;if 在条件成立时执行代码块;== 比较值;= 给名称赋值。缩进是 Python 语法的一部分。这里仅介绍实验所需记法,模块 03 会系统讲解程序设计。

    8

    实验 1 — 阅读执行轨迹

    目标:连接伪代码、状态与实际执行。时间:30 分钟。先预测会打印哪些行,再运行脚本,与下面实际运行捕获的输出比较。为方便对照,两种语言的页面使用相同的代码、书名和输出。

    下载 lab1_trace.py

    """Module 01, Lab 1: trace a search, one comparison at a time."""
    
    books = ["Dune", "Foundation", "The Hobbit", "Dune"]
    target = "The Hobbit"
    found = -1
    
    for index, title in enumerate(books):
        print(f"inspect {index}: {title}")
        if title == target:
            found = index
            break
    
    print(f"result: {found}")
    
    实际运行输出
    inspect 0: Dune
    inspect 1: Foundation
    inspect 2: The Hobbit
    result: 2
    

    found = -1 在开始搜索前设置“未找到”结果。匹配后,break 退出循环,但最后的打印语句仍会执行。引号前的 f 允许把花括号中的值插入输出字符串。

    1. 把目标分别改为 Dune 和 Solaris。运行前先写预测轨迹。
    2. 设置 books = [],解释为什么循环体不执行。
    3. 删除 break 后查询 Dune。为什么结果变成 3?违反了哪条约定?
    4. 恢复原代码,解释“退出循环”与“从函数返回”的区别。
    实验讨论

    Dune 只检查索引 0;Solaris 检查全部四个索引,结果保持 -1。空列表只打印 result: -1。删除 break 后,两次出现 Dune 都会更新 found,最终保留最后一次出现的位置,违反第一个匹配的约定。break 离开循环;return 离开包含它的函数。

    9

    实验 2 — 把约定变成检查

    目标:检查空列表、缺失目标、边界位置与重复值。时间:35 分钟。def 定义函数;return 提供结果并结束本次调用;assert 在条件不成立时终止脚本。正常运行,不要使用会禁用断言的 Python -O 选项。

    下载 lab2_contract.py

    """Module 01, Lab 2: make a contract executable with examples."""
    
    
    def find_first(items, target):
        """Return the first matching index, or -1 if target is absent."""
        for index, item in enumerate(items):
            if item == target:
                return index
        return -1
    
    
    cases = [
        ([], "Dune", -1),
        (["Dune"], "Dune", 0),
        (["Dune"], "Solaris", -1),
        (["Dune", "Solaris"], "Solaris", 1),
        (["Dune", "Solaris", "Dune"], "Dune", 0),
    ]
    
    for items, target, expected in cases:
        actual = find_first(items, target)
        assert actual == expected, (items, target, expected, actual)
        print(f"{items!r}, {target!r} -> {actual}")
    
    print("5 cases passed")
    
    实际运行输出
    [], 'Dune' -> -1
    ['Dune'], 'Dune' -> 0
    ['Dune'], 'Solaris' -> -1
    ['Dune', 'Solaris'], 'Solaris' -> 1
    ['Dune', 'Solaris', 'Dune'], 'Dune' -> 0
    5 cases passed
    
    1. 解释这五个案例分别覆盖了约定的哪些部分。
    2. 增加 (["Dune"], "dune", -1),确认区分大小写的精确匹配。
    3. 把 return -1 移入循环,放在 if 代码块之后。先预测哪些检查失败或结果改变,再运行。
    4. 修复函数。增加一个检查,确认调用后输入列表没有改变。
    实验讨论

    案例覆盖空列表、第一个位置、目标缺失、最后一个位置和重复书名。return -1 放在循环内部时,空列表会直接运行到函数末尾并返回 None,所以第一个断言失败。较后位置的匹配也会被遗漏,因为第一个不匹配后就返回了。检查输入不变性时,先保存 before = items.copy(),调用函数,然后执行 assert items == before。这些例子是有用的证据,但覆盖所有有效输入仍需要不变式推理。

    10

    实验 3 — 测量工作量

    目标:区分输入规模与一次执行的工作量。时间:35 分钟。脚本返回两个值:结果和比较次数。range(size) 产生从零到 size 之前的整数,不包含 size;list 把它们收集起来。同样的相等性搜索过程也适用于数字。+= 1 把计数器增加一。

    下载 lab3_cost.py

    """Module 01, Lab 3: count comparisons instead of timing the computer."""
    
    
    def search_with_cost(items, target):
        comparisons = 0
        for index, item in enumerate(items):
            comparisons += 1
            if item == target:
                return index, comparisons
        return -1, comparisons
    
    
    for size in [0, 1, 4, 8, 16]:
        items = list(range(size))
        missing_index, missing_cost = search_with_cost(items, -1)
        first_index, first_cost = search_with_cost(items, 0)
        print(f"n={size:2}: absent={missing_cost:2}, first={first_cost:2}")
        assert missing_index == -1 and missing_cost == size
        assert first_index == (0 if size else -1)
        assert first_cost == (1 if size else 0)
    
    实际运行输出
    n= 0: absent= 0, first= 0
    n= 1: absent= 1, first= 1
    n= 4: absent= 4, first= 1
    n= 8: absent= 8, first= 1
    n=16: absent=16, first= 1
    
    1. 预测长度 32 的列表中两种查询的比较次数。
    2. 使用目标 size - 1 增加最后一个位置的查询。size 为 0 时,目标仍不存在。
    3. 在长度 16 的列表中查询目标 8。为什么需要九次比较?
    4. 说明非空列表中最佳与最坏比较次数,并解释为什么单凭耗时不能证明这些公式。
    实验讨论

    长度 32 时,目标缺失需要 32 次比较,第一个位置匹配需要 1 次。最后位置匹配时,非空列表需要 size 次比较,空列表需要 0 次。索引 8 是第九个位置,因为从零计数。n 大于零时,最佳是 1 次,最坏是 n 次。实际耗时取决于机器与数据等因素;这些精确次数则直接来自算法步骤。

    11

    练习与完整解答

    先尝试,再展开解答。★ 表示直接应用,★★ 表示结合多个概念,★★★ 表示修改约定或构造推理。

    练习 1 — 规定一个计数问题★8 分钟

    为统计目标书名出现次数写出约定,涵盖空列表与重复书名。

    完整解答

    输入为有限且不变的字符串列表与目标字符串。输出是与目标完全相等的项目数,为非负整数。空列表返回 0,每次重复出现都贡献一次,列表不被修改。

    练习 2 — 跟踪重复值★8 分钟

    按照第一个匹配约定,在 [A, B, A] 中搜索 A。列出检查的索引和结果。最后一个匹配的约定有何不同?

    完整解答

    只检查 0 并返回 0。寻找最后一个匹配必须检查剩余项目;每次匹配都更新结果的完整正向扫描会比较三次并返回 2。

    练习 3 — 修复过早返回★★10 分钟

    搜索在第一个不匹配后立即返回 -1。给出失败输入,并解释修复方法,涵盖空输入。

    完整解答

    输入 [A, B]、目标 B 会失败:A 不匹配,但 B 尚未检查。把表示不存在的返回移到循环之后,仅在全部检查后执行,也使空列表返回 -1,而不是运行到函数末尾返回 None。

    练习 4 — 精确统计工作量★★10 分钟

    列表有 12 项。第一个匹配在索引 7 时比较几次?确认不存在时几次?空列表呢?

    完整解答

    索引 7 需要 8 次比较(索引 0–7);不存在需要 12 次;空列表需要 0 次。这是相等性检查次数,不是全部机器指令数。

    练习 5 — 解释不变式★★★12 分钟

    对于“检查 i 前,更早位置都不包含目标”,给出初始化、保持与退出时的推理。

    完整解答

    初始 i=0 时没有更早位置。i 不匹配后,i+1 前所有位置均不匹配。匹配时没有更早匹配,所以 i 最小;耗尽时所有项目均不匹配。有限长度与位置前进另行证明终止。

    练习 6 — 找出抽象丢失的信息★★10 分钟

    把目录替换为一个项目数量。它能回答哪些问题:有多少项、Dune 在哪里、第一本的书名是什么?

    完整解答

    只能回答项目数。多个不同目录可能有相同数量,因此数量无法还原书名或顺序。抽象是否合适取决于要回答的问题。

    练习 7 — 改变匹配规则★★★15 分钟

    规定并实现一个忽略大小写的 ASCII 书名搜索,返回第一个匹配。哪些检查需要改变?

    完整解答

    要求字符串为 ASCII,在原循环中比较 item.lower() == target.lower(),不存在的返回仍放在循环之后。现在 [Dune] 查询 dune 返回 0;仍需检查空列表、缺失目标与重复值。原来的精确匹配约定已经改变;非 ASCII 文本匹配需要更谨慎的规则。

    def find_first_ascii_ignore_case(items, target):
        for index, item in enumerate(items):
            if item.lower() == target.lower():
                return index
        return -1
    
    assert find_first_ascii_ignore_case(["Dune"], "dune") == 0
    assert find_first_ascii_ignore_case([], "dune") == -1

    练习 8 — 定位故障★★12 分钟

    分类这些故障:要求的匹配规则不对;伪代码跳过索引 0;return -1 缩进在循环内;脚本文件无法打开。

    完整解答

    分别属于规约、算法、实现和环境。相应地,应核实用户要求、修复过程、修复代码控制流,或检查命令与文件路径。故障可能涉及多层,但这一分类有助于确定首先调查哪里。

    12

    自测

    选择答案查看反馈。分数统计已经回答的题目;重置后可以重做。如果禁用脚本,可以展开下方答案表。

    1

    哪个陈述描述问题要求,而不是算法?

    2

    本例搜索空列表返回什么?

    3

    [Dune, Foundation, Dune] 中第一个 Dune 的索引是多少?

    4

    9 个项目中确认目标不存在需要多少次相等性比较?

    5

    哪个事实保证本例搜索终止?

    6

    通过五个测试说明什么?

    7

    为什么用搜索结果索引 Python 列表前要检查 -1?

    8

    有序列表抽象必须保留什么?

    答案表
    1. A — 它描述所需结果,没有规定执行步骤。
    2. B — 约定用 -1 表示不存在,包括空列表。
    3. C — 索引从零开始,算法在第一个匹配处停止。
    4. B — 九个项目都必须比较。最后的返回不属于书名比较。
    5. A — 剩余工作量递减到零;目标可以不存在。
    6. C — 测试为具体案例提供证据;不变式推理覆盖一般有效输入。
    7. B — 本例的哨兵约定与 Python 索引规则含义不同。
    8. A — 第一个匹配搜索需要顺序和项目比较,不需要这些物理细节。
    13

    引导阅读

    阅读官方文档中的 Python 列表介绍,以及 for、range、break 和函数定义。重点阅读实验用到的结构,暂时跳过其他控制流特性。

    写下为什么空列表执行零次循环,为什么 range 不包含终点,以及 return 的作用。然后解释这些语言规则如何支持搜索算法。这次阅读的重点是区分语言规则与问题要求。

    14

    复习与下一步

    现在你能把模糊的搜索请求变成明确约定,跟踪过程,解释为什么返回第一个匹配,说明它如何终止,并统计比较次数。你也能指出这次计算在计算机运行层次中的位置。

    关闭页面,从记忆中写出伪代码。分别跟踪空列表、重复匹配和目标缺失的情况。解释哪个假设保证终止。如果某个解释不确定,先回到相应章节,再勾选最后一个学习时段。

    下一模块是信息表示:位如何编码整数与文字,为什么某些数字无法精确表示。课程概览链接到全部 14 个模块。

    15

    关键术语

    术语本模块中的含义
    计算按照规则转换信息。
    规约输入与输出必须满足的关系。
    算法 / 程序步骤过程 / 编程语言中的实现。
    状态 / 轨迹当前执行信息 / 对它的逐步记录。
    契约操作的前置条件与后置条件。
    不变式迭代过程中在指定位置保持成立的性质。
    终止性过程最终结束。
    抽象暴露相关性质、隐藏细节的模型。
    哨兵值表示不存在等特殊结果的约定值。