Ran Wei/计算机科学系列/05
English
计算机科学基础 — Ran Wei

模块 05: 算法与效率

通过约定、不变式与增长率比较算法,实现二分搜索与插入排序,统计明确操作。

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

完成后你能够

  • 说明二分搜索前置条件。
  • 用不变式论证排序。
  • 区分 O、Ω、Θ。
  • 统计时间与辅助空间。
  • 比较不同输入情况。

开始之前

建议先修模块: 03, 04.

了解循环、有序列表与归纳。log₂ n 表示把 n 反复减半至一所需次数。

目录

学习计划

5 小时

四个 75 分钟时段,包含练习。拓展任务或不熟悉的先修知识可能需要更多时间。进度本地保存,两种语言共享。

时段 275 分钟
构建与探究
时段 375 分钟
应用与拓展
时段 475 分钟
推理与复习
1

成本模型与增长率

计数前明确输入规模与操作。n 个固定大小键的线性搜索最多 n 次比较。O 是常数倍意义下、超过某阈值后的渐近上界,Ω 是下界,Θ 同时是两者。3n+7 为 Θ(n),固定项与常数不改变增长阶;称其 O(n²) 也正确但不紧。需说明分析哪种情况,不能把符号自动等同于最坏情况。

检查理解

3n+7 也是 O(n²) 吗?

完整解答

是,但 Θ(n) 更紧。

2

线性与二分搜索

二分搜索要求键有序且支持高效索引。维护半开区间 [lo,hi),初始 [0,n),检查中点。寻找首个匹配时,中点键较小则 lo=mid+1,否则 hi=mid,保留左侧可能相等项。lo=hi 时检查插入位置。每次探测大致减半,探测次数 O(log n)。对一次查询先排序可能比线性扫描更贵。

检查理解

相等时为何 hi=mid 而非立即返回?

完整解答

保留可能更早的重复匹配。

3

插入排序与不变式

迭代 i 前,前缀 [0,i) 已排序且恰含原前缀项目。取第 i 项为键,把更大项右移,再插入空位,保持不变式。只移动严格更大的键可保留相等键顺序,称稳定排序。有序输入比较 n−1 次,逆序输入比较 n(n−1)/2 次。复制输入版本还分配 n 项输出。

检查理解

长度五的逆序输入比较多少次?

完整解答

1+2+3+4=10。

4

排序选择与下界

归并排序分割、递归排序并线性合并,通常最坏比较为 Θ(n log n),合并存储 O(n)。快速排序按枢轴划分,典型平均 Θ(n log n),差枢轴可达 Θ(n²)。比较排序区分 n! 种排列,最坏需 Ω(log₂(n!))=Ω(n log n) 次比较。计数排序利用受限整数键,因此不违背比较模型下界。

检查理解

计数排序推翻比较排序下界吗?

完整解答

不会,它使用比较之外的键结构。

5

空间、情况与诚实测量

区分输入、输出与辅助工作空间。迭代二分搜索辅助状态 O(1),递归版本栈空间 O(log n)。最佳、最坏、平均依赖不同输入假设,平均需要分布。基准测量需等价输出、可比数据与重复运行。计数揭示增长,耗时揭示常数与环境;两者都不能替代正确性检查。

检查理解

声称平均搜索成本需要什么假设?

完整解答

目标与输入的概率分布。

6

常见误解

  • O 是上界,未必紧。
  • 比较整体流程时需计入排序成本。
7

实验准备

下载脚本,在终端中使用 Python 3.11 或更新版本运行:python m05_search.py. Windows 也可使用 py -3;部分系统使用 python3。实验仅用标准库。先预测结果,再运行并完成变体。不要使用 -O,以保留断言。下方输出由构建器实际运行捕获,两种语言使用相同代码与输出。

8

实验 1 — 二分搜索边界

运行前手动跟踪 lo、hi。probes 统计区间探测,最后相等检查另算。

下载 m05_search.py

"""Binary search maintains a half-open candidate interval [lo, hi)."""
def binary_search(items, target):
    lo, hi, probes = 0, len(items), 0
    while lo < hi:
        mid = (lo + hi) // 2
        probes += 1
        if items[mid] < target:
            lo = mid + 1
        else:
            hi = mid
    found = lo if lo < len(items) and items[lo] == target else -1
    return found, probes

for size in [0, 1, 8, 16, 32]:
    items = list(range(size))
    found, probes = binary_search(items, size)
    assert found == -1
    print(f"n={size:2}, absent insertion point={size:2}, probes={probes}")
assert binary_search([1, 2, 2, 4], 2)[0] == 1
assert binary_search([], 1) == (-1, 0)
print("duplicate case returns first match")
实际运行输出
n= 0, absent insertion point= 0, probes=0
n= 1, absent insertion point= 1, probes=1
n= 8, absent insertion point= 8, probes=3
n=16, absent insertion point=16, probes=4
n=32, absent insertion point=32, probes=5
duplicate case returns first match
  1. 查询十六项的每个值。
  2. 尝试 [2,1] 并说明假设违反。
  3. 把 hi=mid 改为 mid−1,找遗漏边界。
完整解答

每个存在值都应返回索引。[2,1] 无序,约定不适用。hi=mid−1 混合闭区间与半开区间,[1,2] 查二可能遗漏。应始终使用同一约定。

9

实验 2 — 排序与计数

检查空、重复、有序与逆序输入的排序输出。

下载 m05_sort.py

"""Insertion sort counts key comparisons, not elapsed seconds."""
def insertion_sort(values):
    result = values.copy()
    comparisons = 0
    for i in range(1, len(result)):
        key, j = result[i], i - 1
        while j >= 0:
            comparisons += 1
            if result[j] <= key:
                break
            result[j + 1] = result[j]
            j -= 1
        result[j + 1] = key
    return result, comparisons

for values in [[], [1], [1, 2, 3, 4], [4, 3, 2, 1], [2, 1, 2]]:
    result, count = insertion_sort(values)
    assert result == sorted(values)
    print(values, "->", result, "comparisons:", count)
实际运行输出
[] -> [] comparisons: 0
[1] -> [1] comparisons: 0
[1, 2, 3, 4] -> [1, 2, 3, 4] comparisons: 3
[4, 3, 2, 1] -> [1, 2, 3, 4] comparisons: 6
[2, 1, 2] -> [1, 2, 2] comparisons: 2
  1. 运行逆序长度八和十六。
  2. 跟踪相同键的原位置。
  3. 解释复制的内存成本。
完整解答

逆序比较为 28 和 120。<= 停止条件让相同键不互相越过。复制增加 Θ(n) 输出存储,插入步骤本身仅需常数标量状态。

10

练习与完整解答

先尝试,再展开解答。★ 应用概念;★★ 结合概念;★★★ 进行设计或证明。

练习 1 — 增长★

紧确描述 4n²+2n+9。

完整解答

Θ(n²),大 n 时二次项主导。

练习 2 — 二分前提★

为何不能直接二分无序目录?

完整解答

比较无法说明可丢弃哪一半,缺少有序不变式。

练习 3 — 嵌套循环★★

统计 i 遍历 n、j 遍历 i 的次数。

完整解答

总和 n(n−1)/2,所以 Θ(n²)。

练习 4 — 减半★★

64 候选四次减半后剩多少?

完整解答

剩四项,六次到一;精确探测次数依赖边界。

练习 5 — 稳定顺序★★

为何保留相同书名的原顺序?

完整解答

可保留此前按年份等建立的次级顺序。稳定性是约定性质,并非所有排序都有。

练习 6 — 分摊预处理★★★

比较千项的一次与千次查询是否值得排序。

完整解答

一次线性最多千次,排序约万次;多次查询可一次排序后每次对数探测,可能节省。还需考虑更新与常数。

练习 7 — 终止★★★

半开区间二分为何结束?

完整解答

非空时 mid 在区间内,两种更新都严格缩小非负整数长度,最终为零。

练习 8 — 空间统计★★

函数返回列表复制,输出空间多少?

完整解答

Θ(n) 个引用;即使辅助变量常数,也不能称复制版本原地。

11

自测

选择答案查看反馈,重置后可重做。无需 JavaScript 也可阅读答案表。

1

Θ(n) 表示什么?

2

二分要求什么?

3

逆序插入排序成本?

4

稳定排序保留什么?

5

迭代二分辅助空间?

6

平均情况需要什么?

答案表
  1. A — 描述渐近增长。
  2. B — 可通过边界规则处理重复。
  3. C — 每个键越过此前所有键。
  4. A — 相等项保持相对顺序。
  5. B — 保存少量索引。
  6. C — 分布不同平均不同。
12

引导阅读

13

复习与下一步

跟踪五项二分,说明每次丢弃区间的理由,陈述插入排序不变式。模块 06 将研究表示如何影响操作成本。

14

关键术语

术语含义
复杂度资源随输入规模的增长。
稳定性保留相等键相对顺序。
辅助空间除输入与指定输出之外的工作存储。