算法导论(Analysis of Algorithms)
相关链接
1 概述
简述
算法分析
算法分析是关于计算机程序性能和资源利用的研究,主要关注性能(How to make things fast)。
- 在程序设计领域,有什么比性能更重要?
正确性 功能性 模块化 安全性 用户友好
- 为什么研究算法和性能?
例如:性能经常决定是否可用,
预备课程
- 计算机科学与数学基础
- 离散数学
- 概率论
算法复杂度
时间复杂度
在计算机科学中, 算法的时间复杂度是一个函数。用以定量的描述算法的运行时间。用大 O 符号表示,不包括这个函数的低阶项和首项系数,时间复杂度可被称为是渐近的。因为其考察的是当输入值大小趋近无穷时的情况。如:5n^3 + 3n 的时间复杂度,所以其渐近时间复杂度为 O(n^3)
空间复杂度
2 排序
问题 a.
Input sequence <a1, a2, …, an> of number
Output permutation <a1’, a2’, …, an’>
$ a1’ <= a2’ <= … <= an’
2.1 插入排序(Insertion Sort)
代码和执行顺序
Insrertion.Sort(A, n) // Sorts A[1...n]
for j <- 2 to n
do key <- A[j]
i <- j - 1
while i > 0 and A[i]] > key
do A[i+1] <- A[i]
i <- i - 1
A[i+1] <- key粗体表示要插入的数,^表示要插件的位置
^ 8 2 4 9 3 6
2 ^ 8 4 9 3 6
2 ^ 4 8 9 3 6
2 3 4 ^ 8 9 6
2 3 4 6 8 9
运行时间
运算时间取决于:
- 输入参数(eg, 已被 sort, 最坏是被逆序 sort)
- 输入数组的长度
- 输入规模参数化
- 通常想知道运时时间的上界
- 对用户的保证
分析算法的方法
-
最坏情况分析
T(n) = max time on any input of size in. -
平均情况分析
T(n) = The expected time over all inputs of size n, 各种 n 输入的期望情况。
(需要有关输入统计分布的统计假设,比如均匀分布,所有可能的概率相同,取加权的平均值)weighted average - 加权平均
uniform distribution - 均匀分布
-
最好情况分析(bogus)
插入排序的最坏时间
- 计算机性能
- 相对速度(一般比较相同性能,或同一台计算机上)
- 绝对速度(在任何不同的计算机上)
渐进分析法(Asymptotic analysis):
- 忽略依赖机器的常量
- 关注运行时间的增长 当 T(n)中的 n 趋近于无穷大
θ 符号(notation):
去除低阶项,
去除其前面的常数因子
Ex 3n^3 + 90n^2 - 5n + 6046 = θ(n^3)
一个 θ(n^3) 的算法一定会占胜一个 θ(n^2) 的算法
- 插入排序最坏的情况
逆序数组n T(n) = ∑ θ(j) = θ(n^2) j=2
插入排序快吗?n 比较小的时候,插入排序还是挺快的。但 n 的数字变大的时候,插入排序就变显得并不那么快了。
2.2 归并排序
merge sort A[1…n]
- if n = 1, done Time: time θ(1)
- recursively sort A from A[1] up to A[n/2], and A[n/2+1] to A[n] Time: 2T(n/2)
- Merge 2 sorted lists. Time: θ(n)
Key subroutine Merge(关键子程序-merge)
20 12 13 11 7 9 2 1
比较两个序列未取的最小 index 的两个数,哪个最小,则是两个序列当前的最小值。
Time = θ(n) on n total elem S Linear time
Recurrence(递归关系)
| θ(1), if n=1
T(n) = | 2T(n/2) + θ(n), if n > 1
Recursion Tree
T(n) = 2T(n/2) + cn, const c > 0
T(n) = logn * θ(n) = θ(n*logn)
3 渐进符号,递归和解法
3.1 渐进符号
大 O 符号(Big-O notation)
f(n) = O(g(n))
假设 f(n)非负,在适当的常数,c, n0, 使得 0 <= f(n) <= c*g(n), 对于充分大的 n 成立。
Ex: 2n^2 = O(n^3)
大 O 公式的等号不对等,
再定义: f(n)属于 g(n)构成的函数集,
O(g(n)) = { f(n) | 存在常数 n0 > 0, C > 0, 使得 n >= n0 时 0 <= f(n) <= C*g(n) }
所以写说 f(n) ∈ O(n), 例如:2n^2 ∈ O(n^3)
宏(Macro convention)
A set in a formula represents an anonymous function in that set.
Ex: f(n) = n^3 + O(n^2)
There is a function h(n) < O(n^2) such that f(n) = n^3 + h(n)
有低阶项,以某个常数乖以 即 为上界
Ex: n^2 + O(n) = O(n^3) _注意,大 O 在等号不同侧代表含意是相同的,但是等号代表的涵意不同,equal sign is asymmetric _
means for any f(n) ∈ O(n) there is an h(n) ∈ O(n^3) such that n^2 + f(n) = h(n)
总的来说: 大 O 符号很好定义了上界(通常在算法里表示最坏情况),但我们还需要下界(一般在算法里面表示最小值)。
大 Ω 符号(Big Omega-notation)
Ω(g(n)) = {f(n): there exist consts c > 0, n0 > 0 Such that 0 <= c*g(n) <= f(n) for all n >= n0 }
Ex: √n = Ω(logn)
对于充分大的 n, 根号 n 至少是 logn 的常数倍大 Ω 基本对应了大于等于。
大 θ 符号
θ(g(n)) = O(g(n)) ∩ Ω(g(n))
有点像 = 号,但其实不尽然,表示>= 和 <= 之间的渐近
小 o 符号和小 ω
小 o 相当于<
o(g(n)) = { f(n) | 存在常数 n0 > 0,
对于任意 C > 0, 使得 n >= n0 时 0 <= f(n) <= C*g(n) }
Ex: 2n^2 = o(n^3) -> (n0 = 2/c)
小 ω 相当于>
Ω(g(n)) = {f(n):
对于任意c > 0, there exist consts n0 > 0
Such that 0 <= c*g(n) <= f(n)
for all n >= n0 }
Analogies:
| O | Ω | θ |
|---|---|---|
| <= | >= | = |
3.2 Solving recurrences
代换法(substituion method)
- 猜测其大概的形式
- 试着验证满足数学归纳
- 找出常数
Ex: T(n) = 4T(n/2) + n
[T(1) = θ(1)]
T(n) - n = 4T(n/2) = 16T(n/4) = m^2T(n/m) = n^2*T(1) = T(n^2)
注意归纳法在无穷归纳中的应用,大 O 符号不能用于无穷序列
-
Guess: T(n) = O(n^3)
-
Assume T(K) <= c*k^3 for k <= n
-
Tn = 4T(n/2) + n
<= 4c(n/2)^3 + n
= 1/2cn^3 + n
<= cn^3 (根据 Assume,T(n) = cn^3 ) cn^3 - 1/2cn^3 -n >= 0
1/2cn^3 - n >= 0
1/2c*n^2 >= 1 n^2 >= 2/c$ 当 n >= sqrt(2/c) 时,T(n) = O(n^3) 成立。
下面证明紧界(tight bound)
- Assume: T(k) = c*k^2 for k < n
- T(n) = 4(n/2) + n <= 4c(n/2)^2 + n = cn^2 + n
为了使 cn^2 + n < cn^2,必需使得 n 小于零,然而这显然不可能,证明失败!
再次假设:
- Assume T(k) <= c1k^2 - c2k for k < n
- T(n) = 4T(n/2) + n = 4(c1*(n/2)^2) -c2*(n/2) + n = c1n^2 + (1-2c2)n <= c1n^2 - c2*n
c1n^2 - c2n - c1n^2 - (1 - 2c2)*n = (c2 - 1)*n >= 0
$ c2 > 1 时,T(n) = O(n^2) 成立。
还有一点需要考虑到的是 base 情况,当 n = 1 时,
T(1) = c1 - c2,所以 c1 必须大于 c2
递归树(recursion-tree)
Ex:
T(n) = n^2 = n^2 = n^2
| | | | | |
T(n/4) T(n/2) (n/4)^2 (n/2)^2 (n/4)^2 (n/2)^2
| | | | | | | |
T(n/16) T(n/8) T(n/8) T(n/4) (n/16)^2 (n/8)^2 (n/8)^2 (n/4)^2
......................节点数小于n一层一层求和
以上结论是怎么得出的,对于几何级数:
主方法(Master method)
是一个递归树的应用,但更加精准,但只能用于特定的递归式:
T(n) = a*T(n/b) + f(n) a >= 1, b > 1 f(n) 渐近需要趋正
主要思路:对比 非递归函数 f(n) 和 函数
**Case 1: **
f(n) = O() for some ε > 0. (即 指数级小于)
=>
**Case 2: **
for k >= 0 注意 k 的限制条件,若 k < 0 则主方法不适用。(即 f(n) 和 在指数级上一样,但是系数上是一个 log 函数) =>
Case 3: for ε > 0 & af(n/b) <= (1-ε’)f(n) for some ε’ > 0. (即 f(n) 指数级大于 , 后面的条件是为了保证在缩小问题规模时,af(n/b)是渐进趋小的) =>
Ex: T(n) = 4T(n/2) + n
n^logb(a) = n^2 >= n = f(n)
满足 case 1, 所以 T(n) = θ(n^logb(a)) = θ(n^2)
Ex: T(n) = 4T(n/2) + n^2 f(n) = n^2 = n^logb(a)_log^k(n) = n^2 _ log^k(n) -> k = 0
n^logb(a) = n^2 = n^2lg^0(n) = f(n) k = 0 满足 case 2: T(n) = θ(n^logb(a)lg^k+1(n)) = = θ(n^2lg^k+1(n)) 因为 k = 0 = θ(lgnn^2)
Ex: T(n) = 4T(n/2) + n^3 f(n) == n^3 满足 case 3: T(n) = θ(n^3)
Ex: T(n) = 4T(n/2) + n^2/lgn = θ(n^2 * lglgn) try prove:
n^2/lgn
(n/2)^2/lg(n/2) * 4 = n^2/log(n/2)
(n/4)^2/log(n/4) * 16 = n^2/log(n/4)
(n/8)^2/log(n/8) * 64 = n^2/log(n/8)
......
= n^2*log(n/n) = n^2*lg1T(n) = n^2*(lgn + lg(n/2) + lg(n/4) + … + lg(1)) = n^2*(lgnlgn - (lg1 + lg2 + lg4 + lg8 + …+ lgn)) = n^2(lgn*lgn - (1+lgn)*lgn/2) = n^2(lgn*l gn/2 - lgn/2) =
主方法的证明
f(n) f(n/b) f(n/b) f(n/b) f(n/b) 有 a 个
a a a a a a a 有 个
共有 logb(n) 层,所以最后一层是, 各层级的时间为 f(n) af(n/b) 注意这里结合 case3 中要求 af(n/b) <= (1-ε’)f(n/b) f(n)要求余项处于一个收敛的状态 …
将以上各层级结合,看谁占主导,in case3 f(n) 占主导,以下各层以几何级数递减 in case1, θ(n^logba) 占主导,以上各层几何级数递减 **in case2, **f(n) 与 θ(n^logba) 相接近,所以 (因为 中 b 是一个常数,所以
$:
be careful! root n^logb(a) == af(n/b)
so when we compare n^logb(a) and f(n) is actually compare af(n/b) and f(n)‘s residue, so in case you 1: if f(n)‘s residue = O(n^logb(a)), we ignore the residue -> T(n) = n^logb(a)
in case 2: if f(n)‘s residue is very close to af(n/b) = n^logb(a) then find out a constant lg^kn, so that (log^k(n)_f(n)‘s residue) = n^logb(a) -> T(n) = (1 + (lg^k(n))^lg^n) _ n^logb(a) = //TODO
Divide and Conquer
Steps
-
Divide the problem (instance) into one or more sub problems.
-
Conquer each sub problem recursively
-
Combine solutions
Merge sort
Merge sort Running time:
T(n) = 2T(n/2) + θ(n)
使用主方法:
case 2: →
Binary search (find value x in a sorted array)
- Divide: compare x with middle
- Conquer: recurse in one subarray
- Combine: trivial
以下是我自己预先的推导过程: 所以此时处理 case 2:
Powering a number
定义: give number x, integer n >= 0, compute x^n
T(n) = T(n/2) + θ(1) = θ(lgn)
Fibonacci numbers
0 if n = 0Fn = 1 if n = 1 Fn-1 + Fn-2 if n >= 2
F(n) = F(n-1) + F(n-2) = F(n-2) + F(n-3) + F(n-3) + F(n-4) = 2F(n-2) + F(n-3) = 3F(n-3) + 2F(n-4) = 5F(n-4) + 3F(n-3) = 8F(n-5) + 5F(n-4)
形成等比数列,最终证明斐波那契函数为F(n) = 1/sqrt(5) * (((1 + sqrt(5))/2)^2 - ((1 - sqrt(5)/2)^2) = φ^n/sqrt(5) - (1 - φ)^n/sqrt(5)
事实上只需要 n 次笨拙的计算即可得到斐波那契函数的值。然而此处我们要计算算法递归的算法复杂度, 而它使用了自上而下的递归算法,所以非常复杂如下
Naive recursive alg: Time: Ω(φ^n) (φ = (1 + sqrt(5)) / 2)指数级的时候非常差,非常不好。(polynomial)多项式时间算法相对较好
所以我们接下来使用由下至上的方法
Bottom-up algorithm Compute For F1, F2, F3 … Fn
Time: θ(n)
naive recursive squaring
Fn = φ^n/sqrt(5) - (1-φ)^n/sqrt(5) = φ^n/sqrt(5) 这样的话,就可以使用之前所述的使用 lgn 复杂度的方式求,φ^n,然后再除以 sqrt(5)即可。但是:真实计算机中浮点数是缺乏精度的,如果进行近似取整,将得不到正确答案。引出下面的内容
平方递归算法
Thm: (Fn+1 Fn ) = (1 1)^n (Fn Fn-1) (1 0)
=> θ(logn)算法
证明:
Base (1 1)^1 = (F2 F1) (1 0) (F1 F0)
Step: (Fn+1 Fn ) = (Fn Fn-1 )(1 1) (Fn Fn-1) (Fn-1 Fn-2)(1 0) = (Fn + Fn-1, Fn) (Fn-1 + Fn-2, Fn-1) = (Fn+1, Fn) (Fn, Fn-1) = (1 1)^n (1 0)
Matrix multiplications
Input: A = [aij] B = [bij] (i,j = 1…n) Output: C = [cij] = A*B n Cij = ∑ a_ik * b_kj k=1
for i<- 1 to n do for j<-1 to n do for k <- 1 to n cij <- 0 do cij <- cij + aik + bjk
Divide-and-conquer alg: Idea: nn matrix = 22 block matrix of n/2 * n/2 sub matrix.
但这种分法并不会更快,而且会有下标问题。
Strassen’s algorithm(施特拉森) Idea: reduce # musts -> 7 p1 = a(f-h) p2 = (a+b)h p3 = (c+d)e p4 = d(g-e) p5 = (a+d)(e+h) p6 = (b-d)(g+h) p7 = (a-c)(e+f) r = p5 + p4 - p2 + p6 s = p1 + p2 t = p3 + p4 u = p5 + p1 - p3 - p7
u = ae + ah + de + dh
- af - ah
-
ce -de
-
ae - af + ce +cf
= dh + cf
Strassen.
-
Divide A,b compute terms for products θ(n^3)
-
Pi p1 p2 …p3 recursively
-
calculate r s t u θ(n^2)
T(n) = 7T(n/2) + θ(n^2) = θ(n^lg7) = θ(n^2.81) case 1
但这还不是最快的目前最快的约为 θ(n^2.376)
VLSI(very large scale integrated) layout
Problem: Embed a complete binary tree on n leaves in a grid with minimum area
<!--[tree](images/vlsi_tree2.png)-->高度和宽度H(n) ~ logn W(n) ~ n
H(n) = H(n/2) + θ(1) = logn case 2
W(n) = 2W(n/2) + θ(1) = θ(n)
n^logb(a) = n > f(n) case 3
Area = H(n) * W(n) = n*logn
不够好,要进化 Goal:* W(n) = θ(sqrt(n)) H(n) = θ(sqrt(n))
Area = W(n)*H(n) = θ(n)
这种形式必须使用 W(n) 和 H(n) 的 n^logb(a)或者 f(n)为 1/2 指数,并且另一个可忽略:假设一种形式:
W(n) = 2T(n/4) + O(n^(1/2-ε))
如图

L(n) = 2l(n/4) + θ(1) = 0(sqrt(n)) case 1
4 快排和随机算法
快速排序 - Tony Hoare in 1962
- Divide and conquer
- Sorts “in place”
- very practical(with tuning)
Divide and conquer
-
Divide: Partitioning array into 2 subarrays around pivot x $ element in lower subarray <= x <= elements in upper subarray.
-
Conquer: Recursively sort 2 subarrays.
-
Combine: Trivial
Key Linear-time (θ(n)) partitioning subroutine
partition (A, p, q) // A[p-q]
x <- A[p] // pivot = A[p]
i <- p
for j <- p + 1 to q
do if A[j] <= x
then i <- i + 1
exch a[i] <-> a[j]
exch A[p] <-> A[i]
return ip:0 q:5 x:8 i:0 j:1 8 2 4 9 3 6 p:0 q:5 x:8 i:1 j:2 8 2 4 9 3 6 p:0 q:5 x:8 i:2 j:3 8 2 4 9 3 6 p:0 q:5 x:8 i:2 j:4 8 2 4 9 3 6 p:0 q:5 x:8 i:3 j:5 8 2 3 9 4 6 p:0 q:5 x:8 i:4 j:6 8 2 3 6 4 9 end for exch A[p] <-> A[i] 4 2 3 6 8 9
Time = θ(n) for n-element subarray
Ex. 6 10 13 5 8 3 2 11 x=6 i j 6 5 13 10 8 3 2 11 i j 6 5 3 10 8 13 2 11 i j 6 5 3 2 8 13 10 11 i j end for exch x <-> A[i] 2 5 3 6 8 13 10 11
Quicksort(A, p, q) if p < q then r <- Partiion(A, p, q) Quicksort(A, p, r-1) Quicksort(A, r+1, q)
Init call quicksort(A, 1, n)
Analysis - assume all elements distinct
(有重复的无素,这个算法就变得不那么好了,最初的分划方法对相同元素有优化)
T(n) = worst-case time
- input sorted or reverse sorted
- one side of partition has no elements T(n) = T(0) + T(n-1) + θ(n) = θ(1) + T(n-1) + θ(n) = θ(1) + T(n-2) + θ(n-1) + θ(n) = θ(1) + θ(2) + … + θ(n) = θ(n*(n-1/2)) = θ(n^2) (arith series[等差数列], like insertion sort)
快排的重点在于其平均情况非常好
Recursion tree T(n) = T(0) + T(n-1) + cn T(n) = cn = cn T(0) T(n-1) T(0) c(n-1) T(0) T(n-2) = cn T(0) c(n-1) T(0) c(n-2) T(0) c(n-3) .
. . θ(1) 这种递归树是一种高度不平衡的递归树n θ(∑ ck) = θ(n) k=1
Best-case analysis(intuition only)
If we’re really lucky, Partition splits the array n/2 : n/2
T(n) = 2T(n/2) + θ(n) = θ(n*logn)
n^logb(a) = n = (logn)^0 _ n {k = 0} so we are in case 2: T(n) = n _ (logn)^(k+1) = n * logn
Supose split is always 1/10:9/10:
T(n) = T(n/10) + T(9n/10) + θ(n)
T(n) = θ(n) = θ(n) T(n/10) T(9n/)10 θ(n/10) θ(9n/10) T(n/10^2) T(9n/10^2) T(9n/10^2) T(9^2n/10^2)
= θ(n) θ(n/10) θ(9n/10) θ(n/10^2) θ(9n/10^2) θ(9n/10^2) θ(9^2n/10^2)
即时我们发现,树的各分支高度不一,左边最矮为 log10(n), 右边最高为 log(10/9)(n)
然后我们开始,从上至下的求和,会发现,前 log10(n)层之和都为 cn,然后开始有一些层的和小于 cn
T(n) <= cn*log(10/9)(n) + θ(n)
supose we alterrate lucky, unlucky:
L(n) = 2U(n/2) + θ(n) Lucky U(n) = L(n-1) + θ(n) Unlucky
=> L(n) = 2(L(n/2-1) + θ(n/2)) + θ(n) = 2L(n/2 -1) + θ(n) = θ(nlogn) Lucky
Randomized quicksort
-
the running time is independent of input ordering.
-
no assumptions about input distribution
-
no specific input elicit the worse behavior
-
the worst-case is determined only by a random-number generator
**by pivot on round element **
Analysis
T(n) = r. v. for running time assuming the random numbers are independence
For k = 0, 1, …, n-1, let
X_k = 1 if partition generate a k:n-k-1 split 0 otherwise
指示器随机变量(indicator random variable)
X_k 的期望
E[X_k] = 0Pr{X_k=0} + 1Pr{X_k=1} = Pr{X_k=1} = 1/n
引入指示器随机变量,表达式T(k)+T(n-k-1)+θ(n)的累加,直接 X_k = 1 这种分化出现时才有值 n-1
= ∑ X_k(T(k)+T(n-k-1)+θ(n)) k=0
E[T(n)]= E[∑..] n-1 = ∑ E[X_k(T(k)+T(n-k-1)+θ(n))] (期望的和等于和的期望) k=0 n-1 = ∑ E[X_k]*E[T(k)+T(n-k-1)+θ(n)] k=0 = (1/n)∑ E[T(k)]+ (1/n)∑E[T(n-k-1)]+(1/n)∑θ(n) = (2/n)∑ E[T(k)] + θ(n) Absorb k = 0, 1 terms into the θ(n) for tech convenience
Prove {a > 0} Choose a big enough so that anlogn >= E[T(n)] for small n
n-1 Use fact ∑ klogk <= 1/2 n^2logn - 1/8n^2 k=2
用推导法证明:
- 假设 ∑ klogk <= 1/2 n^2logn - 1/8n^2 成立,则有:n-2 ∑ klogk <= 1/2 (n-1)^2log(n-1) - 1/8(n-1)^2 k=2 n-1 n-2 ∑ klogk = ∑ klogk + (n-1)log(n-1) <= 1/2 (n-1)^2log(n-1) - 1/8(n-1)^2 + nlogn k=2 k=2
欲证明 => 0<= 1/2 (n^2 - 2n +1)log(n-1) - 1/8 (n^2 - 2n + 1) + nlogn - nlog(n-1) - n 0<= 1/2n^2log(n-1) - nlog(n-1) + 1/2log(n-1) - 1/8n^2 + 1/4n - 1/8 + nlogn - nlog(n-1) + logn = 1/2n^2log(n-1) + 1/2log(n-1) - 1/8n^2 + 1/4n - 1/2log(n-1) - 1/8 - nlog(n-1) + logn = 1/2n^2*log(n-1) + 1/4n - 1/2log(n-1) - 1/8n^2 - 1/8
n-1 E[T(n)] <= 2/n ∑ aklogk + θ(n) k=1 <= 2a/n∑(1/2n^2logn - 1/8n^2) + θ(n)
<= anlogn - an/4 + θ(n) <= anlogn { if a is big engough so that an/4 dominate θ(n) }
快速排序是一个很不错的算法,其通常比归并排序快三倍以上,虽不然能归并排序那样提供可靠的保证(其在最坏的情况下不是 nlogn,但如果应用 randomdized quicksort),总体来说会快出三倍以上。
besides, 要可能需要在基础规模上做一些优化(coarsen the base cases),还有一些别的技巧,但几乎所有的好的排序算法,都是基于快排的。
它的另一个优势是,在虚拟内存的缓存中运行良好。
5 堆排序
堆排序的时间复杂度是 θ(nlogn),但不同于归并排序,只需要常数个额外存储空间。
5.1 二叉树
二叉树是指每个节点最多有两个子树的树结构,通常称左子树(left subtree)和右子树(right subtree),常被用于二叉查找树和二叉堆。二叉树的每个节点至多只有二棵子树(不存在度大于 2 的节点),二叉树子树有左右之分,次序不能颠倒。二叉树的第 i 层至多有 2^(i-1) 个节点,深度为 k 的二叉树至多有 2^k - 1 个节点。另外,对于任意二叉树,其度为 0 的结点数为 n0(即终端结点数),那么其度为 2 的结点数为 n2,则一定有 n0 = n2 + 1。
满二叉树: 深度为 k,节点数为 2^(k-1)的二叉树即被称为满二叉树。
完全二叉树: 深度为 k 的二叉树,前 k - 1 层节点数达到最大,只在第 k 层不满,但 k 层结点全连续集中在左边。
对于深度为 k 的二叉树,当且仅当其每一个结点都与深度为 k 的满二叉树中编号 1 至 n 结点一一对应时才称之为完全二叉树。
完全二叉树是效率很高的数据结构,堆是一种完全二叉树或者近似完全二叉树,所以效率极高,像十分常用的排序算法,Dijkstra 算法,Prim 算法都要用堆才能优化。二叉排序树的效率也需要借助平衡性来提高,而平衡性基于完全二叉树。
5.2 堆
算法中的堆是指一种数据结构,而非”垃圾收集存储机制”。
二叉堆是一个近似完全二叉树以数组的方式存储起来,除了底层,树是完全充满的。且从左至右填充,如下所示:
如上,根据这样的结构,很容易得到某个元素的父节点,左节点,右节点,这种结构寻找节点,有几个比较固定的方法,在大多计算器中,只需要要通过将下标值移位就能比较简单的得到父、左、右节点的索引,某些语言中,这样的函数通常以”宏”或”内联函数”的方式实现。
最大堆 除根节点外: A[Parent(i)] >= A[i]
最小堆 除根节点外, A[Parent(i)] <= A[i]
堆排序中使用最大堆,最小堆通常 用来构造优先队列。
几个重要结论:MAX_HEAPIFY 过程,其时间复杂度为 O(logn), 它是维护最大堆性质的关键。BUILD_MAX_HEAP 过程,其具有线性时间复杂度,功能是从无序输入数据数组中构造一个最大堆。HEAPSORT 过程,其时间复杂度为 O(n*logn),功能是对一个数组进行原址排序。MAX_HEAP_INSERT,HEAP_EXTRACT_MAX,HEAP_INCREASE_KEY 和 HEAP_MAXIMUM 过程: 时间复杂度为 O(lgn), 功能是利用堆实现一个优先队列。
5.3 维护堆的性质
MAX-HEAPIFY(A, i) l = LEFT(i) r = RIGHT(i) if l < A.heap-size and A[l] > A[i] largest = l else largest = i if r <= A.heap-size and A[r] > A[largest] largest = r if largest != i exchange A[i] with A[largest] MAX-HEAPIFY(A, largest)
MAX_HEAPIFY 时间代价:调整 A[i] A[LEFT(i)] 和 A[RIGHT(i)] 的关系的时间代价 θ(1),加上一棵以 i 的孩子为根结点的子树上运行 MAX_HEAPIFY 的时间代价,一般为 1n/2,最坏的情况为 2n/3,最在最后一层会发生。
T(n) = T(2n/3) + θ(1) n^logb(a) = n^(log(2/3)(1)) = n^0
in Main method case 2: T(n) = θ(logn) 所以当深度为 h 时,其时间复杂度就为 θ(h)
5.4 建堆
BUILD_MAX_HEAP(A): A.heap-size = A.length for i = [A.length/2] downto 1 MAX-HEAPIFY(A, i)
MAX_HEAPIFY 的时间复杂度是 O(lgn), BUILD_MAX_HEAP 需要 O(n)次这样的调用,因些总的时间复杂度是 O(n*lgn)。
进一步求得其紧确界,以下 h 高度从下到上,从 0 开始:
logn logn ∑ (n/2^(h+1))O(h) = O(n ∑ (h/2^h)) h=0 h=0 logn ∞ => n ∑ (h/2^h) = O(∑) C*(1/2)^h = O(n)
h=0 h=0于是我们可以得到 其时间复杂度为
T(n) = O(n)
5.5 堆排序算法
HEAPSORT(A) BUILD_MAX_HEAP(A) for i = A.length downto 2 exchange A[1] with A[i] A.heap-size = A.heap-size - 1 MAX-HEAPIFY(A, 1)
很容易看出 T(n) = θ(nlogn)
5.6 优先队列
堆排序是一个优秀的算法,但在实际应用中,第 7 章将要介绍的快速排序的性能一般会优于堆排序。但,堆这种数据结构仍然有很多应用,高效的优先队列就是其中之一。优先队列也有优先最大队列和优先最小队列两种。
优先队列(Priority queue)是一种用来维护由一组元素构成的集合 S 的数据结构,每个元素有一个相关的的值,key,最大优先队列支持以下操作。INSERT(S,x),S = S U {x} MAXIMUM(S) EXTRACT-MAX(S) 去掉并返回 S 中具有的最大键字元素 INCREASE-KEY(S, x, k) 将元素 x 的关键值增加到 k,这里假设 k 的值不小于 x 的原关键字值。
这个数据结构的应用十分广泛,例 如共享计算机系统的作业高度。最大优先队列记录记录将要执行的各个作业及他们的相对优先级。当一个作业完成或中断后,调 EXTRA_MAX 从所有等待作业选出具最高优先级的作业来执行。
相应的,最小优先队列支持的操作包括:INSERT, MINIUM, EXTRACT_MIN 和 DECREASE_KEY,而这种可能被用于事件驱动的模拟器。
HEAP_MAXIMUM(A): return A[1]
HEAP_EXTRACT_MAX(A): if A.heap-size < 1 error “heap underflow” max = A[1] A[1] = A[A.heap-size] A.heap-size = A.heap-size - 1 MAX-HEAPIFY(A, 1) return max
显然 HEAP_EXTRACT_MAX 的时间复杂度 O(lgn)
HEAP_INCREASE_KEY(A, i, key) if key < A[i] error “new key is smaller than current key” A[i] = key while i > 1 and A[PARENT(i)] < A[i] exchange A[i] with A[PARENT(i)] i = PARENT[i]
HEAP_INCREASE_KEY 的时间复杂度为 O(logn)
MAX_HEAP_INSERT(A, key) A.heap-size = A.heap-size + 1 A[A.heap-size] = - ∞ HEAP_INCREASE_KEY(A, A.heap-。, key) 在包含 n 个元素的最大堆上,MAX_HEAP_INSERT 的运行时间为 O(lgn)
6 速度研究
排序算法能有多快?It depends on what we call the computational model of what you can do with the elements
Quicksort θ(nlogn) randomized Heapsort θ(nlogn) Merge sort θ(nlogn) Insertion sort θ(n^2)
Can we do better than θ(nlogn)?
以上算法,都有一个同样的运算模式,即一次比较 2 个的元素。
6.1 Comparison Sorting(model)
Only use comparisons to determine relative order of elements
如果我们能证明两个元素的比较算法,是不可能慢于 θ(nlogn),那样就比较完美了。
6.2 Decision-tree
Ex: sort <a1, a2, a3> 1:2 2:3 1:3 123 1:3 132 312
In general: <a1, a2 … an> — each internal node has label i:j i,j {1,2,3…n} means: compare a1 vs aj
- left subtree gives subsequent comparisons if ai <= aj
- right subtree ai >= aj each leaf gives permutation
Decision tress comparison sort
— one tree for each n — view algorithm as splitting whenever if makes a comparison — tree lists comparisons along all possible instruction traces
-running time(#comparisions) = length of path
-worst case run time = height of the tree
因此,我们只需要证明决策树的深度为最小为 nlogn 即可。
Lower bound on decision-tree sorting Any decision tree sorting n elements has height 欧米茄(nlogn)
Proof: # leaves must be >= n!
- height h -> n! <= leaves <= 2^h => n! <= 2^h h >= lg n! h >= lg (n/e)^n
= n lg(n/e) = n (logn - loge) = omega (nlogn)
决策树的下界问题,基于比较的排序算法的下界问题。归并排序和堆排序是渐进最优的(这俩都是非随机算法)快速排序在理想情况下也是渐进最好优的
6.3 Sorting in linear time
Counting sort: Input: A{1, n] each A[i] belong {1, ..k } Output: B[1…n] = sorting of A
Auxiliary(辅助) storage: C[1…k] K 是输入序列的范围长度
Counting sort for i <- 1 to k // 初始化长度 k 的数组,每个元素值为 1 do C[i] <- 0 for j <- 1 to n // 将 A[i]的值映射为 C 的坐标 j,并使用相应坐标 j 的值 C[j]为表示 A[i]值出现的次数(频率)do C[A[j]] <- C[A[j]] + 1 // C[i] = |{key = i}|
// 将 C[j]进行转换,表示为 A 中小于等于当前坐标 j 为值的数的个数 // C[j]即成为最后一个值为 j 的元素坐标 for i <- 2 to k
do C[i] <- C[i] + C[i-1] // C[i] = |{ key <= i }|
for j <- n downto 1 // 将 A[j]的值分别放入到对应的座标do B[C[A[j]] <- A[j] C[A[j]] <- C[A[j]] - 1 // 已放置后,将坐标减 1,对于下一个同样值为 A[j]的元素,坐标将减 1
Example:
1 2 3 4 5 1 2 3 4A = 4 1 3 4 3 C = |0|0|0|0| 1 0 2 2 1 1 3 5 -> 做加法,使得 C[A[j]] 表示在 B 中的下标
1 2 3 4 5B = 0 0 0 0 0 1 1 3 5
0 0 0 0 4 1 1 3 4 B[C[A[1]] = A[1] C[A[1]] = C[A[1]] - 1
1 0 0 0 4 0 1 3 4 B[C[A[2]] = A[2] C[A[2]] = C[A[2]] - 1 1 0 3 0 4 0 1 2 4 B[C[A[3]] = A[3] C[A[3]] = C[A[3]] - 1 1 0 3 4 4 0 1 2 3 B[C[A[4]] = A[4] C[A[4]] = C[A[4]] - 1 1 3 3 4 4 0 1 1 3 B[C[A[5] = A[5] C[A[5]] = C[A[5]] - 1
Time: T(n) = O(n + k) k = O(n) then O(n)
Stable sort preserves the relative order of equal elements 即,原数组中,即使是相同元素之前的相对顺序,也没有被改变,因为从 n -> 1 进行放置,第一个遇到的相同元素始终下标最大,然后再对其下标减 1
Exercise: figure out which other sorts are stable?
6.4 Radix sort (Herman Hollerith ~1890)
用以解决在线性时间内对大范围数据进行排序的问题
Example 3 2 9
4 5 7 6 5 7 8 3 9 -> 4 3 6 7 2 0 3 5 5
Sort least digit Sort second digit Sort first digit
Correctness induction on digit position t -assume by induction sorted on low-order t-1 digits - sort on digit t |- if the two elements have same t^th digit,stability => same order 根据刚才的归纳法假设 t-1 是有序的 => 保持不变仍然是有育的 sorted order |- different t^th digit => sorted order.
Analysis - use counting sort for each digit - say n integers each b bits (range = 0 - (2^b - 1) ) - split into b/r “digits” each r bits(2^r)
T(n) = O(b/r) * O(n+k) = O(b/r*(n+2^r))
对r 求导,求导数等于0的解,数学解是最好的
r 越大越好 >=1 时越小越好
b/r * n + b*2^r/r
当 n >= 2^r 时,我们才使用计数算法,取满足这种情况时的r的最大值 r = lgn将 r = lgn 代入
T(n) = O(bn/logn) if numbers in range 0 - 2^b and 0 - n^d -1
then time = O(dn)
计算排序可以处理某个常数乘以 d 范围内的数,但用下面这种方式,可以处理 0 到某个常数次方范围内的数如果 d=O(1) 这就是一个线性时间的算法,所以在 n^logn 范围内,就可能击败 n*logn 的算法
假如有一系列 32 位数,我们把它拆分为 8 位的段,即 r=8 那么会经过 4 个 8 位的 n 次循环,需要 2^8 即 256 位的辅助空间,
但是,计数排序在缓存上有更多的要求,除非数据确实很小。
还有:美好的假设,如果有计算机能在常数时间内对任意整数做计算,那么目前最好的算法的运行时间是 n*sqrt(lglgn) expected
7. 顺序统计、中值
7.1 Order statistics
give n elements in array find kth smallest element (elements of rank k)
传统的方法是直接排序,然后取出第 k 个元素,
但我们希望能不使用排序的方式,并且在线性的时间内完成
k = 1 minimum k = n maximum k = ceil(n+1/2) or floor(n+1/2)
Randomized divide&conquer:
Rand-select(A, p, q, i) // i^th smallest in A[p,q] if p=q then return A[p] r <- Rand-Partition(A, p, q) k <- r - p + 1 // k = rank(A[r]) if i = k then return A[r] if i < k then Rand-select(A, p, r-1, i) if i > k then Rand-select(A, r+1, q, i - k)
intuition for analysis (assume the elements are distinct)
Lucky case: 1/2 : 1/2 | 1/10 : 9/10
T(n) = T(9n/10) + θ(n)
in case 3: T(n) = θ(n)
Unlucky case: 0:n-1 T(n) = T(n-1) + θ(n) T(n) = θ(n^2)
7.2 Analysis of expected time
-
let T(n) be the random variable for running time of Rand-selected on input of size n. assuming random number are independently
-
define indicator random invariable X_k for all k = 0,1…n-1 1 if partition generates a k:n-k-1 split X_k =
0 otherwiseplaintextT(max{0, n-1}) + θ(n) if 0, n-1 splitT(n) <= T(max{1, n-2}) + θ(n) if 1, n-2 split … T(max{n-1, 0}) + θ(n) if n-1, 0 split n-1 = ∑ X*k(T(max{k, n-k-1})) + 0(n) k=0 n-1 E[T(n)] <= E[∑ X_k(T(max{k, n-k-1})) + 0(n)]
k=0 n-1 <= ∑ E[X_k(T(max{k, n-k-1}))} + 0(n)]
k=0 n-1 <= ∑ E[X_K] * E[T(max{k, n-k-1}) + 0(n)]
k=0 n-1 <= ∑ (1/n) _ E[T(max{k, n-k-1}) + 0(n)]
k=0 n-1 n-1 <= 1/n ∑ E[T(max{k, n-k-1})] + 1/n ∑ 0(n)
k=0 k=0 n-1
<= 1/n ∑ E[T(max{k, n-k-1})] + 0(n)
k=0
n-1
<= 2/n ∑ E[T(k))] + 0(n) k=floor(n/2)
不知道怎么证明,想想我们的需要的结论?
需要: T(n) <= 0(n)
不妨设 T(k) <= ck {c 为 >0 的足够大的常数},则有
Proof substitution method Assume true for <n n-1
T(n) <= 2/n ∑ E[ck] + 0(n) k=floor(n/2) n-1 <= 2c/n ∑ k + 0(n) k=floor(n/2) <= 3c*n/4 - c/2 + n
= cn - 3c*n/4 - c/2 - n > 0 for c sufficiently largeRand-Select has 0(n) expected ranging time 0(n^2) worst case
7.3 Worst-case linear-time order statistics
[Blum, Floyd, Proatt, Rivest, Tarjan]
- idea generate good pivot recursivelySelect(i, n): 1. Divide the n elements into floor(n/5) groups of 5 elements each Find the median of each group use 0(n) 2. Recursively select median x of the (n/5) group medians use T(n/5) 3. Partition with x as pivot, let k = rank(x) 4. if i=k then return x if i<k then recursively Select(i, n-k-1) if i>k then recursively Select(i+k, n)
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 高度为 5 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 宽度为 n/5
把以上的矩阵都画上箭头
b ^ | a < b a
= 3 _ floor(n/5)/2 elements <= x and >= x = 3 _ floor(n/10) = 3*n/10
Simplification for n >= 50 3*n/10 >= n/4
So: T(n) <= 0(n) + T(n/5) + T(3n/4) = 0(n) + T(19n/20)
in case 3: T(n) = 0(n) 这种证明方式是错的,T(n/5) + T(3n/4)不能合并,上式已经假设了 T(n) 为线性,即 T(n) = cN,然后将两式合并。
使用代换法: 假设 T(n) <= cn T(n) = cn/5 + c3n/4 + 0(n) = c19/20n + 0(n) <= cn即需要: cn/20 > 0(n) 只需要 c 大于 0(n)的常数的 20 倍即可。
试想一下上方的 5 变为 3
T(n) = cn/3 + c2*floor(n/3) + 0(n) 显然不行
8 Hashing I
8.1 Symbol-table problem
Table s holding n records
key[x] and satellite data
Operations
- Insert(S, x): S <- S u {x}
- Delete(S, x): S <- S - {x}
- Search(S, k): return x $ key[x] = k or nil if no such x
8.2 Direct access table
Suppose keys are drawn from a set of U = {1, 2, 3, 4, …, m-1}
Assume the keys are distinct
Set up array T[0, …, m-1] to represent dynamic set S
x if x ∈ S and its key[x] is k$ T[k] = nil otherwise
Operations take 0(1) in worst case
以上的问题是有大量的空单元,空索引
8.3 Hashing
Hash function h maps keys “randomly” into slots table T
when a record to be inserted maps to an already occupied slots a collision occurs
如果是 cache 的话,把冲撞处的值替换掉即可,
resolving collisions by chaining Idea: link records in same slot into list
EX: slot i -> s1 -> s2 -> s3
independent of where other keys are hashed.Define: The load factor of a hash table with n keys and m slots is α = n/m = average # keys per slots
Expected unsuccessful search time = 0(1 + α) hash search list accessing Expected search time = 0(1) if α = O(1) or if n = O(m)
[8.4 Choosing a hash function]
- Should distribute keys uniformly into slots.
- Regularity in key distribution should not affect uniformity
Division method: h(k) = k mod m don’t pick m with small divisor
Ex: d = 2 and all keys even
=>odd slots never use
Ex: m = 2^r
=>hash doesn’t depend on all bits of k
k = 1011000111011010 如果 r = 6 m = 2^6
在这种情况下,对 k 进行 2^6 取余,只和二进制数的后 6 位有关,这不符合 hash 函数的意图。
pick m to be a prime not too close to a power of two or ten.
因为这是两个最常用的底,很可能在需要处理的数据里面出现规律。
Multiplication method
m = 2^r computer has w-bit words, 32 位或 64 位等
A is an odd integer 2^(w-r) < A < 2^w
rsh is “right shift”
h(k) = (A*k mod 2^w) rsh (w-r)
- Don’t pick A too close to a power of two
- Fast method multiplication mod 2^w is faster than division right shift is fast
Ex: m = 8 = 2^3, w = 7
1 0 1 1 0 0 1 = A
*
1 1 0 1 0 1 1 = K1 0 0 1 0 1 0 0 1 1 0 0 1 1 mod 2^w
0 1 0 1 0 1 1 rsh w-r = 7 - 3 = 4
0 1 0想像 A 是一个小数,A*K 的操作是 K 在乘以一个小数,mod 2^w 以后,得到小数部分(假设小数点在最高位前,实质是没有),所以最后把其右移 w-r 位,使其被限制到 2^r 的槽数中来。
想像一个车轮:Modular wheel 如果把 A 视为一个小数,实质上就是把 A 视为(A/2^8),所以 A 取质数最好,否则也要取奇数,不要接近 2^n K 与 A 相乘实质就会成为 A 的 K 倍,即 2^w 的某倍加一定的余数 1-2^w,再右移 w-r 位回归到 1-2^r 指定插槽的位置。
8.4 Resolving collisions by open addressing
No storage for links
Probe table systematically until an empty slot is found
h 就变为了含有两个参数的函数,key 和 探查序列 h: U X {0, 1, …, m-1} -> {0, 1, …, m-1} universe probe slots key number
Probe seq should be permutation, {0, 1, …, m-1} 的随机排列 Table may fill up so number of elements n <= m Deletion is difficult, but not impossible
Ex: insert k = 496
null null 586 133 null 204 null 481 null
- Probe(496, 0) -> 已有 204 这是随意假设的
- Probe(496, 1) -> 已有 584
- Probe(496, 2) -> 204 与 4l81 之间
Search use same sequence
- successful: find record
- unsuccessful: return nil
Probing strategies
- Linear probing: h(k, i) = (h(k, 0) + i) mod m “primary clustering” long runs of filled slots key 的映射在一段上集中,并且探查就变成了类似于依次查找,个人认为脱离了与 key 值的相关性。
- Double hash只需要预先计算好,h_1(k)和 h_2(k)的值即可h(k, i) = (h_1(k) + i*h_2(k)) mod m excellent method usually pick m = 2^r(因为做 mod 非常好计算) and h_2(k) force to be odd, 否则奇数槽就使用不到了
8.5 Analysis open addressing
Assumption of uniform hashing: each key is equally likely to have any one of the m! permutation as its probe sequence independent of other keys Theorem E[# probes] <= 1/(1-α) α 的值与目前 n 的值与 m 的比例相关
if α < 1 (mean number of keys/elements in the table < m, n < m)
Prove(unsuccessful search)1 probe always necessary with probability n/m, collision -> 2nd probe necessary with probability (n-1)/(m-1), collision -> 3nd probe necessary (n-2)/(m-2) (n-i)/(m-i)<n/m = α
E[# probes] = 1 + n/m(1+(n-1)/(m-1)(1+(n/2)/(m-2)(…(1+1/(m-n))))) <= 1+α(1+α(1+α(1+α(…)))) <= 1+α+a^2+…+ infinity ∞ <= Σ a^i i=0 = 1/(1-a)
- α < 1 const => O(1) probes
- if table 50% full => less than 2 probes
- 90% => less than 10 probes
9. Hashing II
Weakness of hashing For any choice for hash function, there is a bad set of keys that all hash to same slot.
比如,对于已知的 Hash 函数,设定对应的数据输入,就可以使所有的数据都在同一个糟。
Idea: choose hash function at random independently from the keys.
9.1 Universal hashing
Define. Let U be a universe of keys let H be a finite collection of hash function mapping U to slots {0, 1 … m-1} H is universal if all x,y ∈ U where x != y such that: |{h∈H : h(x) = h(y) }| = |H|/m 表示整个 hash function set 中,h(x) = h(y) 的个数期望
Ie. if h is chosen randomly form H, the probability of collision between x and y is ? 1/m 第一数是任意的,第二个数映射到相同 slot 的概率为 1/m 所以,简单来说,每个函数碰撞的概率是 1/m, |H| 个函数里期望有 |m|/H 个函数导致 h(x) = h(y)
theorem: choose h randomly from set, suppose hashing n keys to m slots in table T, then for given keys x, expected number of collision with x E[# collisions with x]< n/m (alpha -> load factor)
prove
Let C_x be random variable denoting the total # of collisions of keys in T with x
let 1 (if h(x) = h(y)) C_xy = 0 (otherwise)
Note: E[C_xy] = 1/m and C_x = ∑ C_xy y∈T-{x}
E[C_x] = E[∑ C_xy] y∈T-{x} = ∑ E[C_xy] (Linearity of expectation) y∈T-{x}
= ∑ 1/m
y∈T-{x}
= (n-1)/mConstructing a universal hash function
Let m be prime, decompose key k into r+1 digits:
K = <k0, k1…, kr> where 0 <= ki <= m-1 , k0 假定为低位,事实上没有关系
上式太复杂,用程序员语言解释就是,把 k 变成 base m (m 进制)的数
Pick a = <a0, a1, …, ar> each ai chosen randomly from {0, 1,…, m-1}
r
Define h_a(k) = (∑ ai*ki) mod m
0每一位的 k 将会被一个 randomized ai 相乘
How big is H=|h_a| ? -> m^(r+1)
Theorem: H is universal
Proof Let x = <x0, x1, …, xr> y = <y0, y1, …, yr> be distinct keys
They differ at least one digit, without lost of generality
position 0 For how many h_a ϵ H hash function do x and y collide.
Must have h_a(x) = h_a(y)
r r => ∑ aixi Ξ ∑ aiyi (mod m) i=0 i=0 r => ∑ ai(xi-yi) Ξ 0 (mod m) i=0 r => a0(x0-y0) + ∑ ai(xi-yi) Ξ 0 (mod m) i=i r => a0(x0-y0) Ξ - ∑ ai(xi-yi) (mod m)
i=1
Number theory fact Let m be prime For any z ϵ Z_m (integers mod m) -> {0, 1, …, m-1}, if z !Ξ 0, there exists a unique z^(-1) ϵ Z_m $ z*z^(-1) Ξ 1 (mod m)
另外的,如果 m 是非质数,这样的性质就不成立了,想像一个,如果 m 等于 10,就找不出这样的数。
而实质上我们通过数论可以知道,如果 z 和 m 不互质(即其 gcd != 1, 或者说有大于 1 的公约数),z 是没有半法求出倒数的。
since x0 != y0, there exists inverse (x0-y0) r => a0 Ξ (- ∑ ai(xi-yi))*(x0-y0)^(-1)
i=0
这是一个很厉害的证明,结论是:当我们假设 distinct x, y 在 h_a(x) = h_y(y) 时,a0 是一个与 ai, xi, yi, 相关的固定值 (mod m)
Thus for any choice of a1, a2, … ar, exactly 1 of the m choices for a0 causes x and y to collide, and no collision for other m-1 choices for a0.
number of h_a’s cause x, y to collide = 1*m^r = |H|/m
9.2 Perfect Hashing
Give n keys, construct a static hash table of size m = O(n), $ search takes O(1) time in the worst case.
Idea: 2-level scheme with universal hashing at both level, No collisions at level 2.

level 1:
h(14) = h(27) = 1
level 2:
h_31(14) = 1 h_31(27) = 2
if n_i items that hash to level 1 slot i, then use m_i = n_i^2 slots to level-2 table s_i
Level-2 analysis
Theorem:
Hash n keys into m = n^2 slots using random h in universal set H => E[#collisions] < 1/2
Prove:
Probability 2 given keys collide under h is 1/m = 1/n^2
in this case we have n chose 2(Cn2 n 在下 2 在上) pair
E[#collisionse] = (n chose 2) 1/n^2 = n*(n-1)/2n^2 < 1/2
Markov inequality
For random variable x >= 0, Probability {x >= t} <= E[x]/t ∞ Pf. E[X] = ∑ xPr{X=x} x=0 ∞ >= ∑ xPr{X=x} x=t ∞ >= ∑ tPr{X=x} x=t
= tPr{x>=t}
Corollary:
Pr{no collisions} >= 1/2
Pf: Pr{>= 1 collision} <=E[#collision]/1
So Pr {no collisions} = 1 - Pr{>= 1 collision} >= 1/2
To find a good level-2 hash function just test a few at random Find one quickly since at least 1/2 will work.
Analysis of storage
For level 1 choose m = n, and let n_i be random variable for # keys hash to slot i in T. Use m_i=n_i^2 slots in each level-2 table slots. n-1 E[total storage] = n + E[∑ θ(n_i^2)] = θ(n) by bucket-sort analysis i=0
Binary Search Tree(BST)
什么是二叉搜索树
搜索数支持许多动态集合操作,包括 SEARCH, MINIMUM, MAXIMUM, PREDECESSOR, SUCCESSOR, INSERT, DELETE 等,即可以做字典,也可以作优先队列。
中序遍历
INORDER-TREE-WALK(x)
if x != NIL
INORDER-TREE-WALK(x.left)
print x.key
INORDER-TREE-WALK(x.right)##查询二叉搜索树
TREE-SEARCH(x, k)
if x == NIL or k == x.key
return x
if k < x.key
return TREE-SEARCH(x.left, k)
else return TREE-SEARCH(x.right, k)从效率起见,用迭代替换递归
INTERATIVE-TREE-SEARCH(x, k)
while x != NIL and k != x.key
if k < x
x = x.left
else
x = x.right
return xTREE-MINIMUM(x)
while x.left != NIL
x = x.left
return xTREE-MAXIMUM(x)
while x.right != NIL
x = x.right
return x后继与前驱
给定一棵二叉搜索树的一个结点,有时候需要按中序遍历查找它的后继:
TREE-SUCCESSOR(x)
if x.right != NIL
return TREE-MINIMUM(x, right)
y = x.p
while y != NIL and x == y.right
x = y
y = y.p
return y相应的,我自己写出 PREDECESSOR 的算法:
TREE-PREDECESSOR(x)
if x.left != NIL
return TREE-MAXIMUM(x)
y = x.p
while y != NIL and y.left == x
x = y
y = y.p
return y插入和删除
插入和删除会引起动态集合变化,插入相对容易,删除相对复杂。
插入
假设输入为一个节点 z, z.key=v, z.left=NIL, z.right=NIL
TREE-INSERT(T, z)
y = NIL
x = T.root
while x != NIL
y = x
if z.key < x.key
x = x.left
else
x = x.right
z.p = y
if y == NIL
T.root = z # tree T was empty
elseif z.key < y.key
y.left = z
else
y.right = z删除
- z 没有孩子结点,简单的删除,修改你结点的子节点为 NIL
- 如果 z 只一个孩子,那么将这个孩子提升到树 z 的位置,并修改 z 的父结点,用 z 的孩子来替换。
- 如果 z 有两个孩子,找 z 的后继 y,并让 y 占据树中 z 的位置。z 的原来右子树部分成 y 的新的右子树,并且 z 的左子树成为 y 的新的左子树。
下面是 TRANSPLANT 的过程
TRANSPLANT(T, u, v)
if u.p == NIL
T.root = v
elseif u == u.p.left
u.p.left = v
else
u.p.right = v
if v != NIL
v.p = u.p下面是删除节点的过程
TREE-DELETE(T, z)
if z.left == NIL
TRANSPLANT(T, z, z.right)
elseif z.right == NIL
TRANSPLANT(T, z, z.left)
else
y = TREE-MINUTE(z.right)
if y.p != z
TRANSPLANT(T, y, y.right)
y.right = z.right
y.right.p = y
TRANSPLANT(T, z, y)
y.left = z.left
y.left.p = y除了 TREE-MINIMUM 之外,TREE-DELETE 的每一行,包括调用 TRANSPLANT,都只花费常数时间。
定理:
在一颗高度为 h 的二叉搜索树上,实现动态集合操作 INSERT 和 DELETE 的运行时间均为 O(h)
Randomly Built Binary Search Tree(BST)
BST sort
BST sort (A): T = nil for i <- to n do Tree-Insert(T, A[i]) Inorder-Tree-Walk(root[T])
Example:
A = [3, 1, 8, 2, 6, 7, 5]
Time: θ(n) + for walk n Tree-Inserts Ω(nlogn) all the time O(n^2)
if already sorted => θ(n)
其在时间复杂度上未随机化的 quciksort 具有一样的性质
Relation to Quicksort
BST sort & Quicksort make same comparisons, but in different order.
以下是快排的过程:
上图可以看出 quicksort 每次选择 pivot 从上至下其实就是二叉树插入的过程。
所以即然快排可以随机化来保证 worst case, BST 也可以, 如下。
Randomized BST Sort
- Randomly permute A
- BST sorted(A)
Time = time(randomized Quicksort)
E[time] = E[time(randomized quicksort)]
Randomly built BST = tree resulting from random BST sort
Time(BST sort) = ∑ depth(x) x∈T
E[1/n ∑depth(x)] = θ(nlogn)/n = θ(lgn) x∈T
上式说明随机搜索树的平均高度是 lgn
Theorem:
E[height of random built BST] = O(lgn)
如果能够证明这个定理,那么,查找必定能在 logn 内完成
Proof outline
- Prove Jenson inequality: f(E[x]) <= E[f(x)]
for convex function f. - Instead of analysing X_n = r.v of height of BST on n nodes, analyse Y_n = 2^X_n
- Prove that E[Y_n] = O(n^3)
- Conclude that E[2^X_n] = E[Y_n]=O(n^3) => 2^E[X_n] <= E[2^X_n] = E[Y_n] = O(n^3) => E[X_n] <= lg(O(n^3)) = 3lgn + O(1)
f:|R->|R is convex if for all x, y ∈ R & all α, β >= 0 with α + β = 1
=> f(αx+βy) <= αf(x)+βf(y)Jenson inequality 的证明太长太复杂,由于对于学习算法并不算特别重要,对其进行省略。从几何上讲,取 convex 函数的两个点,连成直线,其函数区线都在这两个点下方。
Expected BST Height analysis X_n = r.v of height of randomly built BST on n nodes.
Y_n = 2^X_n (这是一个 convex 函数)
if root r has rank k then Xn = 1 + max{X(k-1), X*(n-k)} Y_n = 2 max{Y*(k-1), Y_(n-k)}
define indicator random variable
1 if root has rank kZ_nk = { 0 otherwise
Pr{Z_nk = 1} = E[Z_nk] = 1/n
nYn = ∑ Z_nk(2max{Y(k-1), Y*(n-k)})
k=1 n E[Y_n] = E[∑ Z_nk(2 max{Y*(k-1), Y*(n-k)})] k=1 n = ∑ E[Z_nk(2 max{Y*(k-1), Y*(n-k)})]
k=1 n = 2 ∑ E[Z_nk]E[max{Y*(k-1), Y*(n-k)}] k=1 n <= 2/n ∑ E[Y*(k-1) + Y_(n-k)] k=1 n-1 <= 4/n ∑ E[Y_k] k=0
Claim: E[Y_n] <= 3n^3
Proof, substitution method:
base: cn = θ(1) if c is sufficient large
(induction hypothesis)Step:
n-1E[Y_n] <= 4/n ∑ E[Y_k] k=0
<= 4/n ∑ c*k^3
<= 4c/n integral[0, n]x^3*dx
<= 4c/n * n^4/4
= c*n^3E[X_n] 约= 2.9882×lgn