MIT6.006 notes(3)
- 1MIT6.006 notes(1)
- 2MIT6.006 notes(2)
- 3MIT6.006 notes(3)本文

Lec03 排序
1. 集合的接口
| 类别 | 操作 | 描述 |
|---|---|---|
| 容器 | build(X) | 给定一个可迭代对象 ,利用 中的元素构建集合 |
len() | 返回存储的元素个数 | |
| 静态 | find(k) | 返回键(key)为 的已存储元素 |
| 动态 | insert(x) | 将 添加到集合中(若已存在键为 x.key 的元素,则进行替换) |
delete(k) | 移除并返回键为 的已存储元素 | |
| 顺序 | iter_ord() | 按键的递增顺序逐个返回存储的元素 |
find_min() | 返回具有最小键的已存储元素 | |
find_max() | 返回具有最大键的已存储元素 | |
find_next(k) | 返回键严格大于 的最小已存储元素(后继) | |
find_prev(k) | 返回键严格小于 的最大已存储元素(前驱) |
将元素以任意顺序存储在数组中,可以实现一个(不太高效的)集合。
将元素按键升序排序存储可以带来以下优势:
- 更快地查找最小值/最大值(分别位于数组的首项和末项)
- 通过二分查找实现更快的检索:
数据结构复杂度对比:
| 数据结构 | 容器操作 | 静态操作 | 动态操作 | 顺序操作 |
|---|---|---|---|---|
build(X) | find(k) | insert(x) / delete(k) | find_min() / find_max() | |
| 无序数组 (Array) | ||||
| 有序数组 (Sorted Array) |
(上表所有操作复杂度均为 ** )
2. 如何求解递推式?
代入法: 猜测一个解,用具体函数形式代入,验证递推式是否成立。
递归树法: 画出代表递归调用的树,并将树中各层节点的计算代价累加求和。
主定理: 用于快速求解形如 递推式的通用公式。
3. 如何高效地构建一个有序数组?
有了有序数组,我们便可以利用二分查找来构建高效的集合数据结构。
输入: 包含 个数的(静态)数组 。
输出: (静态)数组 , 是 的一个有序排列。
排列: 包含完全相同元素但顺序可能不同的数组。
有序: 对所有 ,均满足 。
示例:
破坏性排序 : 如果排序直接覆盖原数组 (而不是生成一个新的有序数组 ),则该排序被称为破坏性的。
原地排序: 如果排序仅使用 的额外辅助空间,则称为原地排序(原地排序必然是破坏性的:原地排序 破坏性排序)。
全排列排序
数组 共有 种排列,其中至少有一种排列是有序的。
对每种排列,在 时间内检查其是否已排序。
示例:
def permutation_sort(A):"""对 A 进行排序"""for B in permutations(A): # O(n!) 次循环if is_sorted(B): # O(n)return B # O(1)
分析:
- 正确性: 通过穷举所有可能性进行个案分析(暴力解)。
- 运行时间: ,这是指数级/阶乘级的,极其低效 :(
选择排序
在前缀 中找出最大数,并将其与 交换位置。
递归地对前缀 进行排序。
示例:
def selection_sort(A, i=None): # T(i)"""排序 A[:i + 1]"""if i is None:i = len(A) - 1 # O(1)if i > 0: # O(1)j = prefix_max(A, i) # S(i)A[i], A[j] = A[j], A[i] # O(1)selection_sort(A, i - 1) # T(i - 1)def prefix_max(A, i): # S(i)"""返回 A[:i + 1] 中最大元素的索引"""if i > 0: # O(1)j = prefix_max(A, i - 1) # S(i - 1)if A[i] < A[j]: # O(1)return j # O(1)return i # O(1)
prefix_max 分析:
基本情况: 当 时,数组只有 1 个元素,最大值的索引就是 。
归纳假设: 假设对 成立,则当前最大值要么是 中的最大值,要么是 ,算法在两种情况下均能返回正确索引。
递推关系:
-
代入法: 猜 ,验证 成立。
-
递归树: 深度为 的链,每个节点做 的工作,。
S(n)= S(n-1) + Θ(1)= S(n-2) + Θ(1) + Θ(1)= S(n-3) + Θ(1) + Θ(1) + Θ(1)...= Θ(1) + n 个左右的 Θ(1)= Θ(n)
selection_sort 分析:
基本情况: 当 时,数组只有 1 个元素,天然有序。
归纳假设: 假设selection_sort(A, i-1) 能正确排序 A[:i],证明 selection_sort(A,i) 能正确排序 A[:i+1]。
递推关系:
- 代入法: 猜 ,验证 成立。
- 递归树: 深度为 的链,第 个节点做 的工作,。
插入排序
递归地对前缀 进行排序。
在假定前缀 已排序的前提下,通过连续交换将新元素插入到合适位置,使得前缀 保持有序。
示例:
def insertion_sort(A, i=None): # T(i)"""排序 A[:i + 1]"""if i is None:i = len(A) - 1 # O(1)if i > 0: # O(1)insertion_sort(A, i - 1) # T(i - 1)insert_last(A, i) # S(i)def insert_last(A, i): # S(i)"""在假定 A[:i] 已排序的前提下,排序 A[:i + 1]"""if i > 0 and A[i] < A[i - 1]: # O(1)A[i], A[i - 1] = A[i - 1], A[i]# O(1)insert_last(A, i - 1) # S(i - 1)
insert_last 分析:
基本情况: 当 时,数组只有 1 个元素,已有序。
归纳假设: 假设对 成立。若 ,则整体已有序;否则,交换最后两个元素后,由归纳假设可以继续正确排序 。
递推关系: 。
insertion_sort 分析:
基本情况: 当 时,数组只有 1 个元素,已有序。
归纳假设: 假设对 成立。算法先递归地将 排好序,再利用前面证明的 insert_last 正确处理剩余部分。
递推关系: 。
归并排序
递归地对前半部分和后半部分分别进行排序(可先假设元素个数为 2 的幂)。
将两个已排序的半区合并为一个有序列表(双指针/双指法算法,two finger algorithm)。
示例:
def merge_sort(A, a=0, b=None): # T(b - a = n)"""排序 A[a:b]"""if b is None:b = len(A) # O(1)if 1 < b - a: # O(1)c = (a + b + 1) // 2 # O(1)merge_sort(A, a, c) # T(n / 2)merge_sort(A, c, b) # T(n / 2)L, R = A[a:c], A[c:b] # O(n)merge(L, R, A, len(L), len(R), a, b) # S(n)def merge(L, R, A, i, j, a, b): # S(b - a = n)"""将已排序的 L[:i] 和 R[:j] 合并放入 A[a:b]"""if a < b: # O(1)if (j <= 0) or (i > 0 and L[i - 1] > R[j - 1]): # O(1)A[b - 1] = L[i - 1] # O(1)i = i - 1 # O(1)else: # O(1)A[b - 1] = R[j - 1] # O(1)j = j - 1 # O(1)merge(L, R, A, i, j, a, b - 1) # S(n - 1)
merge 分析:
基本情况: 当 时,数组均为空,结论平凡成立(vacuously correct)。
归纳假设: 假设对规模 成立。填入 的元素必须是 和 剩余未合并前缀中的最大值;由于 和 各自有序,只需比较二者末尾元素取较大者即可;剩下的元素通过归纳假设递归合并完成。
递推关系: 。
merge_sort 分析:
基本情况: 当 时,数组只有 1 个元素,已有序。
归纳假设: 假设对所有 均成立。算法通过归纳假设正确排好两个较小的半区,然后经由上述 merge 函数合并为有序数组。
递推关系:
-
代入法: 猜测
成立。 -
递归树: 深度为 且有 个叶节点的完全二叉树。第 层有 个节点,每个节点耗费 的工作量。
总工作量为:
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!













