类型 | 方法 | |
---|---|---|
容器(container) | build(x) len() |
给定一个可迭代的X,通过X中的项目构建集合 返回存储项目的数量 |
静态(static) | find(k) | 返回键为key的项目 |
动态(dynamic) | insert(x) delete(k) |
添加x到集合(如果已经存在,取代值为x.key的项目) 移除并返回键为key的项目 |
顺序(order) | iter_ord() find_min() find_max() find_next(k) find_prev(k) |
以键值顺序挨个返回存储的项目 返回拥有最小键的项目 返回拥有最大键的项目 返回比k大的最小键项目 返回比k小的最大键项目 |
以任意顺序把项目存到数组中,可以实现一个集合(不是特别有效)
按key升序存储项目允许:
更快找到最大/最小值(数组开头、结尾)
通过二分查找更快查找: O ( l o g n ) \mathcal{O}(logn) O(logn)
数据结构 | 操作,最坏情形O | ||||
容器(container) | 静态(static) | 动态(dynamic) | 顺序(order) | ||
build(x) | find(k) | insert(x) delete(x) |
find_min() find_max() |
find_prev(k) find_next(k) |
|
数组 | n | n | n | n | n |
有序数组 | nlogn | logn | n | 1 | logn |
但如何构建有效地构建有序数组?
给定一个有序数组,我们可以二分查找来构造一个有效的集合数据结构
输入:静态数组A
输出:静态数组B,排序组合后的A
组合:有相同元素、不同顺序的数组
排序: B [ i − 1 ] ≤ B [ i ] , i ∈ { 1 , . . . , n } B[i − 1] ≤ B[i],i \in \{1, . . . , n\} B[i−1]≤B[i],i∈{1,...,n}
举例:[8,2,4,9,3] -> [2,3,4,8,9]
如果覆盖A,那么排序是破坏性的(destructive),不会产生新数组B,而是有序版本的A
如果使用 O ( 1 ) \mathcal{O}(1) O(1)额外空间,那么排序是就地的(in place),就地意味着破坏性, i n p l a c e ⊆ d e s t r u c t i v e in place \subseteq destructive inplace⊆destructive
有n!个A的排列组合,至少其中1个是有序的
对于每种排列组合,检查是否有序: θ ( n ) \theta(n) θ(n)
例子:[2,3,1]->{[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]}
def permutation_sort(A):
'''Sort A'''
for B in permutations(A): # O(n!)
if is_sorted(B) # O(n)
return B # O(1)
permutation_sort分析:
经case分析,这是正确的:尝试所有可能(暴力)
运行时间: Ω ( n ! ⋅ n ) \Omega(n!\cdot n) Ω(n!⋅n),这是指数级的
置换(substitution):猜测一个方案,用代表函数替代,递归成立
递归树(recurrence tree):画一个树代表递归调用,所有节点计算求和
主定理(master theorem):解决一些递归的公式(R03)
找到前A[:i+1]最大的数,交换到A[i]
递归排序前A[:i]
举例:[8,2,4,9,3],[8,2,4,3,9],[3,2,4,8,9],[2,3,4,8,9]
def selection_sort(A, i = None): # T(i)
'''Sort 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)
'''Return index of maximum in 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=0,数组有1个元素,因此最大索引为i
归纳:假设i时正确,最大的要么是A[:i]中最大的、要么是A[i],任意情形返回正确索引
S ( 1 ) = θ ( 1 ) , S ( n ) = S ( n − 1 ) + θ ( 1 ) S(1) = \theta(1),S(n)=S(n-1)+\theta(1) S(1)=θ(1),S(n)=S(n−1)+θ(1)
取代: S ( n ) = θ ( n ) , c n = θ ( 1 ) + c ( n − 1 ) = > 1 = θ ( 1 ) S(n)=\theta(n),cn=\theta(1)+c(n-1) =>1=\theta(1) S(n)=θ(n),cn=θ(1)+c(n−1)=>1=θ(1)
递归树:n个节点的链,每个节点处有 θ ( 1 ) \theta(1) θ(1)工作量, ∑ i = 0 n − 1 = θ ( n ) \sum_{i=0}^{n-1}=\theta(n) ∑i=0n−1=θ(n)
selection_sort分析:
基本情形:对于i=0,数组有一个元素,因此是有序的
归纳:假设i时正确,有序输出的最后一个数是数组中最大的数,算法把它放在了那,那么根据归纳A[:i]是有序的
T ( 1 ) = θ ( 1 ) , T ( n ) = T ( n − 1 ) + θ ( n ) T(1) = \theta(1),T(n)=T(n-1) + \theta(n) T(1)=θ(1),T(n)=T(n−1)+θ(n)
取代: T ( n ) = θ ( n 2 ) , c n 2 = θ ( n ) + c ( n − 1 ) 2 = > c ( 2 n − 1 ) = θ ( n ) T(n)=\theta(n^2),cn^2=\theta(n)+c(n-1)^2=>c(2n-1)=\theta(n) T(n)=θ(n2),cn2=θ(n)+c(n−1)2=>c(2n−1)=θ(n)
递归树:n个节点的链,每个节点处有 θ ( n ) \theta(n) θ(n)工作量, ∑ i = 0 n − 1 = θ ( n 2 ) \sum_{i=0}^{n-1}=\theta(n^2) ∑i=0n−1=θ(n2)
递归排序前A[:i]
排序前A[:i+1],假设前A[:i]被重复交换进而有序
举例:[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]
def insertion_sort(A, i = None): # T(i)
'''Sort 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)
'''Sort A[:i+1] assuming sorted A[:i]'''
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=0,数组有一个元素,因此是有序的
归纳:假设i时成立,如果A[i] >= A[i - 1],数组是有序的。否则交换最后两个元素,允许我们通过归纳对A[:i]排序
S ( 1 ) = θ ( 1 ) , S ( n ) = S ( n − 1 ) + θ ( 1 ) = > S ( n ) = θ ( n ) S(1)=\theta(1),S(n)=S(n-1)+\theta(1)=>S(n)=\theta(n) S(1)=θ(1),S(n)=S(n−1)+θ(1)=>S(n)=θ(n)
insertion_sort分析:
基本情形:对于i=0,数组有一个元素因此是有序的
归纳:假设i时成立,根据归纳算法排序A[:i],然后insert_last正确地排序(正如上面证明的那样)
T ( 1 ) = θ ( 1 ) , T ( n ) = T ( n − 1 ) + θ ( n ) = > T ( n ) = θ ( n 2 ) T(1)=\theta(1),T(n)=T(n-1)+\theta(n)=>T(n)=\theta(n^2) T(1)=θ(1),T(n)=T(n−1)+θ(n)=>T(n)=θ(n2)
递归排序前半部分和后半部分(可能假设是2的指数)
归并排序,把两个半边合成一个有序list
举例:[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]
def merge_sort(A, a = 0, b = None): # T(b - a = n)
'''Sort 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)
'''Merge sorted L[:i] and R[:j] into 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=0,数组为空,完全正确
归纳:假设n时正确,A[r]必须是剩下的L、R前面的中最大的数,因此他们是有序的,找出后面满足元素中最大的。剩下的根据归纳被合并。
S ( 0 ) = θ ( 1 ) , S ( n ) = S ( n − 1 ) + θ ( 1 ) = > S ( n ) = θ ( n ) S(0)=\theta(1),S(n)=S(n-1)+\theta(1)=>S(n)=\theta(n) S(0)=θ(1),S(n)=S(n−1)+θ(1)=>S(n)=θ(n)
merge_sort分析:
基本情形:对于n=1,数组没有元素,因此是有序的
归纳:假设k T ( 1 ) = θ ( 1 ) , T ( n ) = 2 T ( n / 2 ) + θ ( n ) T(1)=\theta(1),T(n)=2T(n/2)+\theta(n) T(1)=θ(1),T(n)=2T(n/2)+θ(n) 取代:假设 T ( n ) = θ ( n l o g n ) , c n l o g n = θ ( n ) + 2 c ( n / 2 ) l o g ( n / 2 ) = > c n l o g ( 2 ) = θ ( n ) T(n)=\theta(nlogn),cnlogn=\theta(n)+2c(n/2)log(n/2)=>cnlog(2)=\theta(n) T(n)=θ(nlogn),cnlogn=θ(n)+2c(n/2)log(n/2)=>cnlog(2)=θ(n) 递归树:二叉树有深度: l o g 2 n log_2n log2n,叶子:n,level i有 2 i 2^i 2i个节点,每个节点需要 O ( n / 2 i ) \mathcal{O}(n/2^i) O(n/2i)工作,总共 ∑ i = 0 l o g 2 n ( 2 i ) ( n / 2 i ) = ∑ i = 0 l o g 2 n n = θ ( n ) \sum_{i=0}^{log_2n}(2^i)(n/2^i)=\sum_{i=0}^{log_2n}n=\theta(n) ∑i=0log2n(2i)(n/2i)=∑i=0log2nn=θ(n) 回想在recitation2中,我们将Set接口简化为Sequence接口(我们用一个模拟另外一个)。这直接由数组提供Set数据结构(尽管很简陋)。 我们想做地更好,我们将花费5节lecture/recitation,来尝试精准地实现它。获取一个更快的集合,最简单的方式之一是:把我们的项目存储到一个有序数组中,有最小key的项目出现在头部(index 0),有最大key的项目出现在尾部。我们能简单地二分查找来找key,且支持有序操作。这对于动态操作仍然不太好(在数组中插入、删除时,items仍然需要被移动),但通过他们的key找items是更快的!首先,我们如何获取一个有序数组呢? 对可比较项目构成的数组A进行升序排序,是众多计算问题中的常见子任务。插入排序和选择排序是常见的少量项目排序算法,因为它们易于理解和实现。两个算法都是递增的,它们保存、增长一个有序的项目子集,直到所有项目有序。它们之间的不同是细微的: 选择排序,保存、增长一个子集,最大的i个项目有序地处于其中 选择排序,保存、增长一个子集,前i个项目是有序的 这是选择排序的python实现。已经让处于子数组A[i+1:]中的最大项目有序,算法重复地扫描数组,找出尚未排序的最大的项目,并使其与项目item[i]交换。正如从代码中看到的,选择排序会需要 Ω ( n 2 ) \Omega(n^2) Ω(n2)次比较,但最坏情形下最多执行 O ( n ) \mathcal{O}(n) O(n)次交换。 这是插入排序的python实现。已经让子数组A[:i]有序,算法重复地交换项目A[i]与其左侧,直到左侧项目不再大于A[i]。正如可在代码中看到的那样,最坏情形下,插入排序需要 Ω ( n 2 ) \Omega(n^2) Ω(n2)次比较、 Ω ( n 2 ) \Omega(n^2) Ω(n2)次交换。 插入排序和选择排序都是就地算法,意味着它们每个,可以用常量额外空间被实现。执行在数组上的仅有操作是比较,以及成对元素之间的交换。插入排序是稳定的,意味着:拥有相同值的项目,将以与输入数组相同顺序,出现在排序中。通过比较,选择排序实现是非稳固的。例如:输入(2,1,1’)会产生输出(1`,1,2)。 在本课中,我们介绍归并排序(merge sort),一个用于排序更大数量项目的更快算法。这个算法是递归排序数组左半部分和右半部分,然后以线性时间归并两半部分。归并排序的递归关系是 T ( n ) = 2 T ( n / 2 ) + θ ( n ) T(n)=2T(n/2)+\theta(n) T(n)=2T(n/2)+θ(n),得到 T ( n ) = θ ( n l o g n ) T(n)=\theta(nlogn) T(n)=θ(nlogn)。比起次方, θ ( n l o g n ) \theta(nlogn) θ(nlogn)渐近增长率是更接近线性的,正如 l o g n logn logn指数性慢于n。特别地, l o g n logn logn增长慢于任意 n ϵ ( ϵ > 0 ) n^\epsilon(\epsilon>0) nϵ(ϵ>0)。 当组合两个半边时,归并排序使用线性数量的临时存储,因此它并非in-place。已经存在算法执行归并,不使用额外空间,这个实现比起归并排序算法更复杂。归并排序是否是稳固的,取决于归并时实现如何打破绑定。上面的实现是非稳固的,但它用小小的改变可以变得稳固。 有3个解决递归的主要方法: 取代:猜测一个答案,代入递归表达式 递归树:画一颗树,代表递归、节点处的求和运算。这是一个非常通用的方法,也是至今我们在课上用到过的。 主定理:解决大量递归的通用公式,它是有用的,但也是难以被记住的。 主定理提供一种解决递归关系的方式,递归调用通过常量因子降低问题尺寸。给定递归关系: T ( n ) = a T ( n / b ) + f ( n ) , T ( 1 ) = θ ( 1 ) T(n)=aT(n/b)+f(n),T(1)=\theta(1) T(n)=aT(n/b)+f(n),T(1)=θ(1),分支因子a>=1,问题尺寸减少因子b>1,以及渐近性非负函数 f ( n ) f(n) f(n),主定理通过比较 f ( n ) f(n) f(n)和 a l o g b n = n l o g b a a^{log_bn}=n^{log_ba} alogbn=nlogba(递归树底部叶子数量)。当 f ( n ) f(n) f(n)渐进式增长快于 n l o g b a n^{log_ba} nlogba,每级要做的工作几何减少,因此工作由根部控制;当 f ( n ) f(n) f(n)增长更慢,每级要做的工作几何增长,工作由叶子控制。当它们的增长率是可比较的,工作平摊在树的 O ( l o g n ) \mathcal{O}(logn) O(logn)叶子上。 T ( n ) = θ ( n l o g b a ) ,条件: f ( n ) = O ( n l o g b a − ϵ ) , ϵ > 0 T ( n ) = θ ( n l o g b a l o g k + 1 n ) ,条件: f ( n ) = O ( n l o g b a l o g k n ) , k > = 0 T ( n ) = θ ( f ( n ) ) ,条件: f ( n ) = O ( n l o g b a + ϵ ) , ϵ > 0 且 a f ( n / b ) < c f ( n ) , 0 < c < 1 T(n)=\theta(n^{log_ba}),条件:f(n)=\mathcal{O}(n^{log_ba-\epsilon}),\epsilon>0 \\T(n)=\theta(n^{log_ba}log^{k+1}n),条件:f(n)=\mathcal{O}(n^{log_ba}log^kn),k>=0 \\T(n)=\theta(f(n)),条件:f(n)=\mathcal{O}(n^{log_ba+\epsilon}),\epsilon>0且af(n/b) 主定理采用一个更简单的形式,当f(n)是多项式,递归: T ( n ) = a T ( n / b ) + θ ( n c ) T(n)=aT(n/b)+\theta(n^c) T(n)=aT(n/b)+θ(nc) T ( n ) = θ ( n l o g b a ) , c < l o g b a , 叶子支配 T(n)=\theta(n^{log_ba}),c T ( n ) = θ ( n c l o g n ) , c = l o g b a , 整个树支配 T(n)=\theta(n^clogn),c=log_ba,整个树支配 T(n)=θ(nclogn),c=logba,整个树支配 T ( n ) = θ ( n c ) , c > l o g b a , 根支配 T(n)=\theta(n^c),c>log_ba,根支配 T(n)=θ(nc),c>logba,根支配 特殊情形直接由取代法证明(这可以在recitation中完成)。为了应用主定理(或更简单的特殊情形),你应该说明哪种情形适用,并且表示:你的递归关系满足相关case的所有条件。甚至有更强的公式来解决递归,但我们不在本课使用。 写一个递归用于二分查找并解决它 T ( n ) = T ( n / 2 ) + O ( 1 ) T(n)=T(n/2)+\mathcal{O}(1) T(n)=T(n/2)+O(1) T ( n ) = O ( log n ) T(n)=\mathcal{O}(\log n) T(n)=O(logn),符合情形2 T ( n ) = T ( n − 1 ) + O ( 1 ) T(n)=T(n-1)+\mathcal{O}(1) T(n)=T(n−1)+O(1) T ( n ) = O ( n ) T(n)=\mathcal{O}(n) T(n)=O(n),长度为n的链,每个节点 O ( 1 ) \mathcal{O}(1) O(1) T ( n ) = T ( n − 1 ) + O ( n ) T(n)=T(n-1)+\mathcal{O}(n) T(n)=T(n−1)+O(n) T ( n ) = O ( n 2 ) T(n)=\mathcal{O}(n^2) T(n)=O(n2),长度为n的链,高度为k时,每个节点 O ( k ) \mathcal{O}(k) O(k)工作 T ( n ) = 2 T ( n − 1 ) + O ( 1 ) T(n)=2T(n-1)+\mathcal{O}(1) T(n)=2T(n−1)+O(1) T ( n ) = O ( 2 n ) T(n)=\mathcal{O}(2^n) T(n)=O(2n),高度为n的二叉树,每个节点 O ( 1 ) \mathcal{O}(1) O(1)工作 T ( n ) = T ( 2 n / 3 ) + O ( 1 ) T(n)=T(2n/3)+\mathcal{O}(1) T(n)=T(2n/3)+O(1) T ( n ) = O ( log n ) T(n)=\mathcal{O}(\log n) T(n)=O(logn),长度为 l o g 3 / 2 ( n ) log_{3/2}(n) log3/2(n)的链,每个节点 O ( 1 ) \mathcal{O}(1) O(1)工作 T ( n ) = 2 T ( n / 2 ) + O ( 1 ) T(n)=2T(n/2)+\mathcal{O}(1) T(n)=2T(n/2)+O(1) T ( n ) = O ( n ) T(n)=\mathcal{O}(n) T(n)=O(n),高度为 l o g 2 n log_2n log2n的二叉树,每个节点 O ( 1 ) \mathcal{O}(1) O(1)工作 T ( n ) = T ( n / 2 ) + O ( n ) T(n)=T(n/2)+\mathcal{O}(n) T(n)=T(n/2)+O(n) T ( n ) = O ( n ) T(n)=\mathcal{O}(n) T(n)=O(n),长度为 l o g 2 n log_2n log2n的链,高度为k时,每个节点 O ( 2 k ) \mathcal{O}(2^k) O(2k)工作 T ( n ) = 2 T ( n / 2 ) + O ( n l o g n ) T(n)=2T(n/2)+\mathcal{O}(nlogn) T(n)=2T(n/2)+O(nlogn) T ( n ) = O ( n l o g 2 n ) T(n)=\mathcal{O}(nlog^2n) T(n)=O(nlog2n))(主定理的特殊情形不适用,因为nlogn不是多项式),高度为 l o g 2 n log_2n log2n的二叉树,高度为k时,每个节点 O ( k ∗ 2 k ) \mathcal{O}(k*2^k) O(k∗2k)工作 T ( n ) = 4 T ( n / 2 ) + O ( n ) T(n)=4T(n/2)+\mathcal{O}(n) T(n)=4T(n/2)+O(n) T ( n ) = n 2 T(n)=n^2 T(n)=n2,高度 l o g 2 n log_2n log2n的4叉树,高度为k时,每个节点 O ( 2 k ) \mathcal{O}(2^k) O(2k)工作八、详述(recitation)
数据结构
操作,最坏情形O
容器(container)
静态(static)
动态(dynamic)
顺序(order)
build(X)
find(k)
insert(x)
delete(k)find_min()
find_max()find_prev(k)
find_next(k)
数组
n
n
n
n
n
数据结构
操作,最坏情形O
容器(container)
静态(static)
动态(dynamic)
顺序(order)
build(X)
find(k)
insert(x)
delete(k)find_min()
find_max()find_prev(k)
find_next(k)
有序数组
?
logn
n
1
logn
class Sorted_Array_Set:
def __init__(self): self.A = Array_Seq() #O(1)
def __len__(self): return len(self.A) #O(1)
def __iter__(self): yield from self.A #O(n)
def iter_order(self): yield from self #O(n)
def build(self, X): #O(?)
self.A.build(X)
self._sort()
def _sort(self):
?? #O(?)
def _binary_search(self, k, i, j): #O(log n)
if i >= j: return i
m = (i + j) // 2
x = self.A.get_at(m)
if x.key > k: return self._binary_search(k, i, m - 1)
if x.key < k: return self._binary_search(k, m + 1, j)
return m
def find_min(self): #O(1)
if len(self) > 0: return self.A.get_at(0)
else: return None
def find_max(self):
if len(self) > 0: return self.A.get_at(len(self) - 1)
else: return None
def find(self, k): # O(log n)
if len(self) == 0 return None
i = self._binary_search(k, 0, len(self) - 1)
x = self.A.get_at(i)
if x.key == k: return x
else: return None
def find_next(self, k):
if len(self) == 0: return None
i = self._binary_search(k, 0, len(self) - 1)
x = self.A.get_at(i)
if x.key > k: return x
if i + 1 < len(self): return self.A.get_at(i+1)
else: return None
def find_prev(self, k):
if len(self) == 0: return None
i = self._binary_search(k, 0, len(self) - 1)
x = self.A.get_at(i)
if x.key < k: return x
if i > 0: return self.A.get_at(i - 1)
else: return None
def insert(self, x):
if len(self.A) == 0:
self.A.insert_first(x)
else:
i = self._binary_search(x.key, 0, len(self.A) - 1)
k = self.A.get_at(i).key
if k == x.key:
self.a.set_at(i, x)
return False
if k > x.key: self.A.insert_at(i, x)
else: self.A.insert_at(i, x)
return True
def delete(self, k):
i = self._binary_search(k, 0, len(self.A) - 1)
assert self.A.get_at(i).key == k
return self.A.delete_at(i)
排序(Sorting)
选择排序(Selection Sort)
def selection_sort(A):
for i in range(len(A) - 1, 0, -1):
m = i
for j in range(i)
if A[m] < A[j]:
m = j
A[m], A[i] = A[i], A[m]
插入排序(Insertion Sort)
def insertion_sort(A):
for i in range(1, len(A)
j = i
while j > 0 and A[j] < A[j - 1]:
A[j - 1], A[j] = A[j], A[j - 1]
j = j - 1
in-place和stability
归并排序
def merge_sort(A, a = 0, b = None):
if b is None
b = len(A)
if 1 < b - a
c = (a + b + 1) // 2
merge_sort(A, a, c)
merge_sort(A, c, b)
L, R = A[a:c], A[c:b]
i, j = 0, 0
while a < b:
if (j > = len(R)) or (i < len(L) and L[i] < R[j]):
A[a] = L[i]
i = i + 1
else:
A[a] = R[j]
j = j + 1
a = a + 1
递归
习题