成本模型与增长率
计数前明确输入规模与操作。n 个固定大小键的线性搜索最多 n 次比较。O 是常数倍意义下、超过某阈值后的渐近上界,Ω 是下界,Θ 同时是两者。3n+7 为 Θ(n),固定项与常数不改变增长阶;称其 O(n²) 也正确但不紧。需说明分析哪种情况,不能把符号自动等同于最坏情况。
3n+7 也是 O(n²) 吗?
完整解答
是,但 Θ(n) 更紧。
线性与二分搜索
二分搜索要求键有序且支持高效索引。维护半开区间 [lo,hi),初始 [0,n),检查中点。寻找首个匹配时,中点键较小则 lo=mid+1,否则 hi=mid,保留左侧可能相等项。lo=hi 时检查插入位置。每次探测大致减半,探测次数 O(log n)。对一次查询先排序可能比线性扫描更贵。
相等时为何 hi=mid 而非立即返回?
完整解答
保留可能更早的重复匹配。
插入排序与不变式
迭代 i 前,前缀 [0,i) 已排序且恰含原前缀项目。取第 i 项为键,把更大项右移,再插入空位,保持不变式。只移动严格更大的键可保留相等键顺序,称稳定排序。有序输入比较 n−1 次,逆序输入比较 n(n−1)/2 次。复制输入版本还分配 n 项输出。
长度五的逆序输入比较多少次?
完整解答
1+2+3+4=10。
排序选择与下界
归并排序分割、递归排序并线性合并,通常最坏比较为 Θ(n log n),合并存储 O(n)。快速排序按枢轴划分,典型平均 Θ(n log n),差枢轴可达 Θ(n²)。比较排序区分 n! 种排列,最坏需 Ω(log₂(n!))=Ω(n log n) 次比较。计数排序利用受限整数键,因此不违背比较模型下界。
计数排序推翻比较排序下界吗?
完整解答
不会,它使用比较之外的键结构。
空间、情况与诚实测量
区分输入、输出与辅助工作空间。迭代二分搜索辅助状态 O(1),递归版本栈空间 O(log n)。最佳、最坏、平均依赖不同输入假设,平均需要分布。基准测量需等价输出、可比数据与重复运行。计数揭示增长,耗时揭示常数与环境;两者都不能替代正确性检查。
声称平均搜索成本需要什么假设?
完整解答
目标与输入的概率分布。
常见误解
- O 是上界,未必紧。
- 比较整体流程时需计入排序成本。
实验准备
下载脚本,在终端中使用 Python 3.11 或更新版本运行:python m05_search.py. Windows 也可使用 py -3;部分系统使用 python3。实验仅用标准库。先预测结果,再运行并完成变体。不要使用 -O,以保留断言。下方输出由构建器实际运行捕获,两种语言使用相同代码与输出。
实验 1 — 二分搜索边界
运行前手动跟踪 lo、hi。probes 统计区间探测,最后相等检查另算。
"""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
- 查询十六项的每个值。
- 尝试 [2,1] 并说明假设违反。
- 把 hi=mid 改为 mid−1,找遗漏边界。
完整解答
每个存在值都应返回索引。[2,1] 无序,约定不适用。hi=mid−1 混合闭区间与半开区间,[1,2] 查二可能遗漏。应始终使用同一约定。
实验 2 — 排序与计数
检查空、重复、有序与逆序输入的排序输出。
"""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
- 运行逆序长度八和十六。
- 跟踪相同键的原位置。
- 解释复制的内存成本。
完整解答
逆序比较为 28 和 120。<= 停止条件让相同键不互相越过。复制增加 Θ(n) 输出存储,插入步骤本身仅需常数标量状态。
练习与完整解答
先尝试,再展开解答。★ 应用概念;★★ 结合概念;★★★ 进行设计或证明。
紧确描述 4n²+2n+9。
完整解答
Θ(n²),大 n 时二次项主导。
为何不能直接二分无序目录?
完整解答
比较无法说明可丢弃哪一半,缺少有序不变式。
统计 i 遍历 n、j 遍历 i 的次数。
完整解答
总和 n(n−1)/2,所以 Θ(n²)。
64 候选四次减半后剩多少?
完整解答
剩四项,六次到一;精确探测次数依赖边界。
为何保留相同书名的原顺序?
完整解答
可保留此前按年份等建立的次级顺序。稳定性是约定性质,并非所有排序都有。
比较千项的一次与千次查询是否值得排序。
完整解答
一次线性最多千次,排序约万次;多次查询可一次排序后每次对数探测,可能节省。还需考虑更新与常数。
半开区间二分为何结束?
完整解答
非空时 mid 在区间内,两种更新都严格缩小非负整数长度,最终为零。
函数返回列表复制,输出空间多少?
完整解答
Θ(n) 个引用;即使辅助变量常数,也不能称复制版本原地。
自测
选择答案查看反馈,重置后可重做。无需 JavaScript 也可阅读答案表。
Θ(n) 表示什么?
二分要求什么?
逆序插入排序成本?
稳定排序保留什么?
迭代二分辅助空间?
平均情况需要什么?
答案表
- A — 描述渐近增长。
- B — 可通过边界规则处理重复。
- C — 每个键越过此前所有键。
- A — 相等项保持相对顺序。
- B — 保存少量索引。
- C — 分布不同平均不同。
引导阅读
- MIT 算法课程 — 阅读排序与二分资料,指出各自前置条件。
复习与下一步
跟踪五项二分,说明每次丢弃区间的理由,陈述插入排序不变式。模块 06 将研究表示如何影响操作成本。
关键术语
| 术语 | 含义 |
|---|---|
| 复杂度 | 资源随输入规模的增长。 |
| 稳定性 | 保留相等键相对顺序。 |
| 辅助空间 | 除输入与指定输出之外的工作存储。 |