CHARLORÉ / AQUATIC MEMORY

Charloré

MIT6.006 notes(3)

2152 字
11 分钟
MIT6.006 notes(3)

Lec03 排序#

1. 集合的接口#

类别操作描述
容器build(X)给定一个可迭代对象 XX,利用 XX 中的元素构建集合
len()返回存储的元素个数
静态find(k)返回键(key)为 kk 的已存储元素
动态insert(x)将 xx 添加到集合中(若已存在键为 x.key 的元素,则进行替换)
delete(k)移除并返回键为 kk 的已存储元素
顺序iter_ord()按键的递增顺序逐个返回存储的元素
find_min()返回具有最小键的已存储元素
find_max()返回具有最大键的已存储元素
find_next(k)返回键严格大于 kk 的最小已存储元素(后继)
find_prev(k)返回键严格小于 kk 的最大已存储元素(前驱)

将元素以任意顺序存储在数组中,可以实现一个(不太高效的)集合。

将元素按键升序排序存储可以带来以下优势:

  • 更快地查找最小值/最大值(分别位于数组的首项和末项)
  • 通过二分查找实现更快的检索:O(log⁡n)O(\log n)

数据结构复杂度对比:

数据结构容器操作静态操作动态操作顺序操作
build(X)find(k)insert(x)​ / delete(k)find_min()​ / find_max()
无序数组 (Array)nnnnnnnn
有序数组 (Sorted Array)nlog⁡nn \log nlog⁡n\log nnn11

(上表所有操作复杂度均为 *O(⋅)O(\cdot)*​ )

2. 如何求解递推式?#

代入法: 猜测一个解,用具体函数形式代入,验证递推式是否成立。

递归树法: 画出代表递归调用的树,并将树中各层节点的计算代价累加求和。

主定理: 用于快速求解形如 T(n)=aT(n/b)+f(n)T(n) = aT(n/b) + f(n) 递推式的通用公式。

3. 如何高效地构建一个有序数组?#

有了有序数组,我们便可以利用二分查找来构建高效的集合数据结构。

输入: 包含 nn 个数的(静态)数组 AA。

输出: (静态)数组 BB,BB 是 AA 的一个有序排列。

排列: 包含完全相同元素但顺序可能不同的数组。

有序: 对所有 i∈{1,…,n}i \in \{1, \dots, n\},均满足 B[i−1]≤B[i]B[i - 1] \le B[i]。

示例: [8,2,4,9,3]→[2,3,4,8,9][8, 2, 4, 9, 3] \to [2, 3, 4, 8, 9]

破坏性排序 : 如果排序直接覆盖原数组 AA(而不是生成一个新的有序数组 BB),则该排序被称为破坏性的。

原地排序: 如果排序仅使用 O(1)O(1) 的额外辅助空间,则称为原地排序(原地排序必然是破坏性的:原地排序 ⊆\subseteq 破坏性排序)。

全排列排序#

数组 AA 共有 n!n! 种排列,其中至少有一种排列是有序的。

对每种排列,在 Θ(n)\Theta(n) 时间内检查其是否已排序。

示例: [2,3,1]→{[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]}[2, 3, 1] \to \{[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]\}

def permutation_sort(A):
"""对 A 进行排序"""
for B in permutations(A): # O(n!) 次循环
if is_sorted(B): # O(n)
return B # O(1)

分析:

  • 正确性: 通过穷举所有可能性进行个案分析(暴力解)。
  • 运行时间: Ω(n!⋅n)\Omega(n! \cdot n),这是指数级/阶乘级的,极其低效 :(

选择排序#

在前缀 A[:i+1]A[:i + 1] 中找出最大数,并将其与 A[i]A[i] 交换位置。

递归地对前缀 A[:i]A[:i] 进行排序。

示例: [8,2,4,9,3]→[8,2,4,3,9]→[3,2,4,8,9]→[3,2,4,8,9]→[2,3,4,8,9][8, 2, 4, 9, 3] \to [8, 2, 4, 3, 9] \to [3, 2, 4, 8, 9] \to [3, 2, 4, 8, 9] \to [2, 3, 4, 8, 9]

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​ 分析:

基本情况: 当 i=0i = 0 时,数组只有 1 个元素,最大值的索引就是 ii。

归纳假设: 假设对 i−1i-1 成立,则当前最大值要么是 A[:i]A[:i] 中的最大值,要么是 A[i]A[i],算法在两种情况下均能返回正确索引。

递推关系: S(1)=Θ(1),S(n)=S(n−1)+Θ(1)S(1) = \Theta(1), \quad S(n) = S(n - 1) + \Theta(1)

  • 代入法: 猜 S(n)=Θ(n)S(n) = \Theta(n),验证 cn=Θ(1)+c(n−1)  ⟹  c=Θ(1)cn = \Theta(1) + c(n - 1) \implies c = \Theta(1) 成立。

  • 递归树: 深度为 nn 的链,每个节点做 Θ(1)\Theta(1) 的工作,∑i=0n−11=Θ(n)\sum_{i=0}^{n-1} 1 = \Theta(n)。

    S(n)
    = S(n-1) + Θ(1)
    = S(n-2) + Θ(1) + Θ(1)
    = S(n-3) + Θ(1) + Θ(1) + Θ(1)
    ...
    = Θ(1) + n 个左右的 Θ(1)
    = Θ(n)

selection_sort​ 分析:

基本情况: 当 i=0i = 0 时,数组只有 1 个元素,天然有序。

归纳假设: 假设selection_sort(A, i-1)​ 能正确排序 A[:i]​,证明 selection_sort(A,i)​ 能正确排序 A[:i+1]。

递推关系: T(1)=Θ(1),T(n)=T(n−1)+Θ(n)T(1) = \Theta(1), \quad T(n) = T(n - 1) + \Theta(n)

  • 代入法: 猜 T(n)=Θ(n2)T(n) = \Theta(n^2),验证 cn2=Θ(n)+c(n−1)2  ⟹  c(2n−1)=Θ(n)cn^2 = \Theta(n) + c(n - 1)^2 \implies c(2n - 1) = \Theta(n) 成立。
  • 递归树: 深度为 nn 的链,第 ii 个节点做 Θ(i)\Theta(i) 的工作,∑i=0n−1i=Θ(n2)\sum_{i=0}^{n-1} i = \Theta(n^2)。

插入排序#

递归地对前缀 A[:i]A[:i] 进行排序。

在假定前缀 A[:i]A[:i] 已排序的前提下,通过连续交换将新元素插入到合适位置,使得前缀 A[:i+1]A[:i + 1] 保持有序。

示例: [8,2,4,9,3]→[2,8,4,9,3]→[2,4,8,9,3]→[2,4,8,9,3]→[2,3,4,8,9][8, 2, 4, 9, 3] \to [2, 8, 4, 9, 3] \to [2, 4, 8, 9, 3] \to [2, 4, 8, 9, 3] \to [2, 3, 4, 8, 9]

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​ 分析:

基本情况: 当 i=0i = 0 时,数组只有 1 个元素,已有序。

归纳假设: 假设对 ii 成立。若 A[i]≥A[i−1]A[i] \ge A[i - 1],则整体已有序;否则,交换最后两个元素后,由归纳假设可以继续正确排序 A[:i]A[:i]。

递推关系: S(1)=Θ(1),S(n)=S(n−1)+Θ(1)  ⟹  S(n)=Θ(n)S(1) = \Theta(1), \quad S(n) = S(n - 1) + \Theta(1) \implies S(n) = \Theta(n)。

insertion_sort​ 分析:

基本情况: 当 i=0i = 0 时,数组只有 1 个元素,已有序。

归纳假设: 假设对 ii 成立。算法先递归地将 A[:i]A[:i] 排好序,再利用前面证明的 insert_last 正确处理剩余部分。

递推关系: T(1)=Θ(1),T(n)=T(n−1)+Θ(n)  ⟹  T(n)=Θ(n2)T(1) = \Theta(1), \quad T(n) = T(n - 1) + \Theta(n) \implies T(n) = \Theta(n^2)。

归并排序#

递归地对前半部分和后半部分分别进行排序(可先假设元素个数为 2 的幂)。

将两个已排序的半区合并为一个有序列表(双指针/双指法算法,two finger algorithm)。

示例: [7,1,5,6,2,4,9,3]→[1,7,5,6,2,4,3,9]→[1,5,6,7,2,3,4,9]→[1,2,3,4,5,6,7,9][7, 1, 5, 6, 2, 4, 9, 3] \to [1, 7, 5, 6, 2, 4, 3, 9] \to [1, 5, 6, 7, 2, 3, 4, 9] \to [1, 2, 3, 4, 5, 6, 7, 9]

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​ 分析:

基本情况: 当 n=0n = 0 时,数组均为空,结论平凡成立(vacuously correct)。

归纳假设: 假设对规模 nn 成立。填入 A[b−1]A[b-1] 的元素必须是 LL 和 RR 剩余未合并前缀中的最大值;由于 LL 和 RR 各自有序,只需比较二者末尾元素取较大者即可;剩下的元素通过归纳假设递归合并完成。 □\square

递推关系: S(0)=Θ(1),S(n)=S(n−1)+Θ(1)  ⟹  S(n)=Θ(n)S(0) = \Theta(1), \quad S(n) = S(n - 1) + \Theta(1) \implies S(n) = \Theta(n)。

merge_sort​ 分析:

基本情况: 当 n=1n = 1 时,数组只有 1 个元素,已有序。

归纳假设: 假设对所有 k<nk < n 均成立。算法通过归纳假设正确排好两个较小的半区,然后经由上述 merge 函数合并为有序数组。 □\square

递推关系: T(1)=Θ(1),T(n)=2T(n/2)+Θ(n)T(1) = \Theta(1), \quad T(n) = 2T(n/2) + \Theta(n)

  • 代入法: 猜测 T(n)=Θ(nlog⁡n)T(n) = \Theta(n \log n)
    ​cnlog⁡n=Θ(n)+2c(n/2)log⁡(n/2)  ⟹  cnlog⁡(2)=Θ(n)cn \log n = \Theta(n) + 2c(n/2)\log(n/2) \implies cn \log(2) = \Theta(n) 成立。

  • 递归树: 深度为 log⁡2n\log_2 n 且有 nn 个叶节点的完全二叉树。第 ii 层有 2i2^i 个节点,每个节点耗费 O(n/2i)O(n/2^i) 的工作量。
    总工作量为:

    ∑i=0log⁡2n(2i)⋅(n2i)=∑i=0log⁡2nn=Θ(nlog⁡n)\sum_{i=0}^{\log_2 n} (2^i) \cdot \left(\frac{n}{2^i}\right) = \sum_{i=0}^{\log_2 n} n = \Theta(n \log n)

‍

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

MIT6.006 notes(3)
https://charlore.cn/posts/mit6.006-notes-3/
作者
路人蛃
发布于
2026-09-17
许可协议
CC BY-NC-SA 4.0

评论区