《算法设计与分析》复习提纲
文章目录
《算法设计与分析》复习提纲
- 中科大算法设计与分析(xuy),复习提纲;
- 个人整理,大部分来自之前的算法导论笔记;
- 《算法导论》专栏地址:算法导论第3版专栏
1 引言(ch1)
1.1 什么是算法及其特征
通俗地说,算法是任何明确定义的计算过程,它将一些值或一组值作为输入,并在有限的时间内产生一些值或一组值作为输出。因此,算法是将输入转换为输出的一系列计算步骤。
算法的特征:输入、输出、确定性、有限性、正确性、通用性。
1.2 问题实例和问题规模
- 问题实例是计算问题解决方案所需要的所有输入。
- 问题规模是算法的输入实例大小。
2 算法初步(ch3)
2.1 插入排序算法
2.2 算法复杂度及其度量
-
时间复杂性和空间复杂性
-
最坏、最好和平均情形复杂性
2.3 插入排序额最坏、最好和平均时间
- 最坏: O ( n 2 ) \Omicron(n^2) O(n2)。
- 最好: O ( n ) \Omicron(n) O(n)。
- 平均时间: O ( n 2 ) \Omicron(n^2) O(n2)。
2.4 归并排序算法及其时间复杂度
时间复杂度 O ( n log n ) \Omicron(n \log n) O(nlogn)。
3 函数增长率(ch3)
3.1 O \Omicron O符号, Ω \Omega Ω符号, Θ \Theta Θ符号
O \Omicron O符号
对于给定的函数 g ( n ) g(n) g(n),
O ( g ( n ) ) = { f ( n ) : ∃ c > 0 , n 0 > 0 , ∀ n ≥ n 0 , 0 ≤ f ( n ) ≤ c g ( n ) } \Omicron(g(n))=\{f(n):\exist \ c \gt 0, \ n_0 \gt 0,\ \forall n \ge n_0, \ 0 \le f(n) \le cg(n) \} O(g(n))={f(n):∃ c>0, n0>0, ∀n≥n0, 0≤f(n)≤cg(n)}
f ( n ) ∈ O ( g ( n ) ) f(n)\in \Omicron(g(n)) f(n)∈O(g(n)),一般书写为 f ( n ) = O ( g ( n ) ) f(n)= \Omicron(g(n)) f(n)=O(g(n)),计算可转化为 0 ≤ f ( n ) ≤ c g ( n ) 0 \le f(n) \le cg(n) 0≤f(n)≤cg(n)
Ω \Omega Ω符号
对于给定的函数 g ( n ) g(n) g(n),
Ω ( g ( n ) ) = { f ( n ) : ∃ c > 0 , n 0 > 0 , ∀ n ≥ n 0 , 0 ≤ c g ( n ) ≤ f ( n ) } \Omega(g(n))=\{f(n):\exist \ c \gt 0, \ n_0 \gt 0,\ \forall n \ge n_0, \ 0 \le cg(n) \le f(n) \} Ω(g(n))={f(n):∃ c>0, n0>0, ∀n≥n0, 0≤cg(n)≤f(n)}
f ( n ) ∈ Ω ( g ( n ) ) f(n)\in \Omega(g(n)) f(n)∈Ω(g(n)),一般书写为 f ( n ) = Ω ( g ( n ) ) f(n)= \Omega(g(n)) f(n)=Ω(g(n)),计算可转化为 0 ≤ c g ( n ) ≤ f ( n ) 0 \le cg(n) \le f(n) 0≤cg(n)≤f(n)
Θ \Theta Θ符号
对于给定的函数 g ( n ) g(n) g(n),
Θ ( g ( n ) ) = { f ( n ) : ∃ c 1 > 0 , c 2 > 0 , n 0 > 0 , ∀ n ≥ n 0 , 0 ≤ c 1 g ( n ) ≤ f ( n ) ≤ c 2 g ( n ) } \Theta(g(n))=\{f(n):\exist c_1 \gt 0, \ c_2 \gt 0, \ n_0 \gt 0,\ \forall n \ge n_0, \ 0 \le c_1g(n) \le f(n) \le c_2g(n) \} Θ(g(n))={f(n):∃c1>0, c2>0, n0>0, ∀n≥n0, 0≤c1g(n)≤f(n)≤c2g(n)}
f ( n ) ∈ Θ ( g ( n ) ) f(n)\in \Theta(g(n)) f(n)∈Θ(g(n)),一般书写为 f ( n ) = Θ ( g ( n ) ) f(n)= \Theta(g(n)) f(n)=Θ(g(n)),计算可转化为 c 1 g ( n ) ≤ f ( n ) ≤ c 2 g ( n ) c_1g(n) \le f(n) \le c_2g(n) c1g(n)≤f(n)≤c2g(n)
定理:对于任意两个函数 f ( n ) f(n) f(n)和 g ( n ) g(n) g(n),当且仅当 f ( n ) = O ( g ( n ) ) f(n)=\Omicron(g(n)) f(n)=O(g(n))且 f ( n ) = Ω ( g ( n ) ) f(n)=\Omega(g(n)) f(n)=Ω(g(n))时,有 f ( n ) = Θ ( g ( n ) ) f(n)=\Theta(g(n)) f(n)=Θ(g(n))
3.2 标准复杂性函数及其大小关系
O ( 1 ) < O ( log n ) < O ( n ) < O ( n log n ) < O ( n 2 ) < O ( n 3 ) < O ( 2 n ) < O ( n ! ) < O ( n n ) \Omicron(1) \lt \Omicron(\log n) \lt \Omicron(n) \lt \Omicron(n \log n) \lt \Omicron(n^2) \lt \Omicron(n^3) \lt \Omicron(2^n) \lt \Omicron(n!) \lt \Omicron(n^n) O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)
3.3 和式界的证明方法
- 放缩法
- 极限法
- 积分法
- …
4 递归关系式(ch4,Sch1)
4.1 代入法
代入法求解递归式分两步:
- 猜测解的形式。(猜测依靠“经验”,偶尔需要创造力)
- 用数学归纳法求出解中的常数,并证明解是正确的。
例题:求解 T ( n ) = 2 T ( ⌊ n / 2 ⌋ ) + n T(n)=2T(\lfloor n/2 \rfloor)+n T(n)=2T(⌊n/2⌋)+n的上界。
证明:猜测其解
T
(
n
)
=
O
(
n
log
n
)
T(n)=\Omicron(n\log n)
T(n)=O(nlogn),代入法要求证明
∃
c
>
0
,
n
0
>
0
,
∀
n
≥
n
0
,
0
≤
T
(
n
)
≤
c
n
log
n
\exist \ c \gt 0, \ n_0 \gt 0,\ \forall n \ge n_0, \ 0 \le T(n) \le cn\log n
∃ c>0, n0>0, ∀n≥n0, 0≤T(n)≤cnlogn,又因为
T
(
⌊
n
/
2
⌋
)
≤
c
⌊
n
/
2
⌋
log
(
⌊
n
/
2
⌋
)
T(\lfloor n/2 \rfloor) \le c\lfloor n/2 \rfloor \log(\lfloor n/2 \rfloor)
T(⌊n/2⌋)≤c⌊n/2⌋log(⌊n/2⌋),代入到递归式中,得到
T
(
n
)
≤
2
(
c
⌊
n
/
2
⌋
log
(
⌊
n
/
2
⌋
)
)
+
n
≤
c
n
log
(
n
/
2
)
+
n
=
c
n
log
n
−
c
n
+
n
≤
c
n
log
n
,
其中
c
≥
1
T(n) \le 2(c\lfloor n/2 \rfloor \log(\lfloor n/2 \rfloor))+n \le cn\log(n/2)+n \\=cn\log n-cn+n \\ \le cn\log n,其中c \ge 1
T(n)≤2(c⌊n/2⌋log(⌊n/2⌋))+n≤cnlog(n/2)+n=cnlogn−cn+n≤cnlogn,其中c≥1
4.2 迭代法
在递归树中,每个结点表示一个单一子问题的代价,子问题对应某次递归函数的调用。我们将树中每层中的代价求和,得到每层代价,然后将所有层的代价求和,得到所有层次递归调用的总代价。
递归式最适合用来生成好的猜测,然后即可用代入法来验证猜测是否正确。
例如, T ( n ) = 3 T ( ⌊ n / 4 ⌋ ) + Θ ( n 2 ) T(n)=3T(\lfloor n/4 \rfloor)+\Theta(n^2) T(n)=3T(⌊n/4⌋)+Θ(n2),
T
(
n
)
=
c
n
2
+
3
16
c
n
2
+
(
3
16
)
2
c
n
2
+
⋯
+
(
3
16
)
log
4
n
c
n
2
+
Θ
(
n
log
4
3
)
=
∑
i
=
0
log
4
n
(
3
16
)
i
c
n
2
+
Θ
(
n
log
4
3
)
<
16
13
c
n
2
+
Θ
(
n
log
4
3
)
=
O
(
n
2
)
T(n)=c n^2+\frac{3}{16} c n^2+\left(\frac{3}{16}\right)^2 c n^2+\cdots+\left(\frac{3}{16}\right)^{\log _4 n} c n^2+\Theta\left(n^{\log _4 3}\right) \\ =\sum_{i=0}^{\log _4 n}\left(\frac{3}{16}\right)^i c n^2+\Theta\left(n^{\log _4 3}\right) \\ <\frac{16}{13} c n^2+\Theta\left(n^{\log _4 3}\right) \\ =O\left(n^2\right)
T(n)=cn2+163cn2+(163)2cn2+⋯+(163)log4ncn2+Θ(nlog43)=i=0∑log4n(163)icn2+Θ(nlog43)<1316cn2+Θ(nlog43)=O(n2)
可以用代入法验证下递归树法的结果:
猜测 T ( n ) ≤ d n 2 T(n) \le dn^2 T(n)≤dn2,则 T ( n ) = 3 T ( n / 4 ) + Θ ( n 2 ) ≤ 3 d ( n / 4 ) 2 + c n 2 = 3 16 d n 2 + c n 2 ≤ d n 2 ,其中 n ≥ 16 13 c T(n)=3T(n/4)+\Theta(n^2) \le 3d(n/4)^2+cn^2= \frac{3}{16}dn^2+cn^2 \le dn^2,其中n \ge \frac{16}{13}c T(n)=3T(n/4)+Θ(n2)≤3d(n/4)2+cn2=163dn2+cn2≤dn2,其中n≥1316c。得证。
4.3 主定理
假设有递推式 T ( n ) = a T ( n b ) + f ( n ) T(n)=aT(\frac{n}{b})+f(n) T(n)=aT(bn)+f(n),其中 a ≥ 1 , b > 1 a \ge 1, b\gt 1 a≥1,b>1, n n n为问题规模, a a a为递归的子问题数量, n b \frac{n}{b} bn为每个子问题的规模(假设每个子问题的规模基本一样), f ( n ) f(n) f(n)为递归以外进行的计算工作。那么 T ( n ) T(n) T(n)有如下渐近界:
- 若存在常数 ϵ > 0 \epsilon \gt 0 ϵ>0,有 f ( n ) = O ( n log b a − ϵ ) f(n)=\Omicron(n^{\log_ba-\epsilon}) f(n)=O(nlogba−ϵ),则 T ( n ) = Θ ( n log b a ) T(n)=\Theta(n^{\log_ba}) T(n)=Θ(nlogba)。
- 若 f ( n ) = O ( n log b a ) f(n)=\Omicron(n^{\log_ba}) f(n)=O(nlogba),则 T ( n ) = Θ ( n log b a log n ) T(n)=\Theta(n^{\log_ba}\log n) T(n)=Θ(nlogbalogn)。
- 若存在常数 ϵ > 0 \epsilon \gt 0 ϵ>0,有 f ( n ) = Ω ( n log b a + ϵ ) f(n)=\Omega(n^{\log_ba+\epsilon}) f(n)=Ω(nlogba+ϵ),且同时存在常数 c < 1 c \lt 1 c<1以及足够大的 n n n满足 a f ( n b ) ≤ c f ( n ) af(\frac{n}{b}) \le cf(n) af(bn)≤cf(n),则 T ( n ) = Θ ( f ( n ) ) T(n)=\Theta(f(n)) T(n)=Θ(f(n))。
例如, T ( n ) = 9 T ( n / 3 ) + n T(n)=9T(n/3)+n T(n)=9T(n/3)+n,对于这个递归式,有 a = 9 , b = 3 , f ( n ) = n a=9,b=3,f(n)=n a=9,b=3,f(n)=n,因此 n log b a = n log 3 9 = Θ ( n 2 ) n^{\log_ba}=n^{\log_39}=\Theta(n^2) nlogba=nlog39=Θ(n2)。由于 f ( n ) = O ( n log 3 9 − ϵ ) f(n)=\Omicron(n^{\log_39-\epsilon}) f(n)=O(nlog39−ϵ),其中 ϵ = 1 \epsilon=1 ϵ=1,满足情况一,从而得到解 T ( n ) = Θ ( n 2 ) T(n)=\Theta(n^2) T(n)=Θ(n2)。
例如, T ( n ) = T ( 2 n / 3 ) + 1 T(n)=T(2n/3)+1 T(n)=T(2n/3)+1,对于这个递归式,有 a = 1 , b = 3 / 2 , f ( n ) = 1 a=1,b=3/2,f(n)=1 a=1,b=3/2,f(n)=1,因此 n log b a = n log 3 / 2 1 = n 0 = 1 n^{\log_ba}=n^{\log_{3/2}1}=n^0=1 nlogba=nlog3/21=n0=1。由于 f ( n ) = Θ ( n log b a ) = Θ ( 1 ) f(n)=\Theta(n^{\log_ba})=\Theta(1) f(n)=Θ(nlogba)=Θ(1),满足情况二,从而得到解 T ( n ) = Θ ( log n ) T(n)=\Theta(\log n) T(n)=Θ(logn)。
例如, T ( n ) = 3 T ( n / 4 ) + n log n T(n)=3T(n/4)+n\log n T(n)=3T(n/4)+nlogn,对于这个递归式,有 a = 3 , b = 4 , f ( n ) = n log n a=3,b=4,f(n)=n\log n a=3,b=4,f(n)=nlogn,因此 n log b a = n log 4 3 = Θ ( n 0.793 ) n^{\log_ba}=n^{\log_43}=\Theta(n^{0.793}) nlogba=nlog43=Θ(n0.793)。由于 f ( n ) = Ω ( n log 4 3 + ϵ ) f(n)=\Omega(n^{\log_43+\epsilon}) f(n)=Ω(nlog43+ϵ),其中 ϵ ≈ 0.2 \epsilon\approx0.2 ϵ≈0.2,当 n n n足够大时,对于 c > 3 / 4 c \gt 3/4 c>3/4, a f ( n / b ) = 3 ( n / 4 ) log ( n / 4 ) ≤ ( 3 / 4 ) n log n = c f ( n ) af(n/b)=3(n/4)\log(n/4) \le(3/4)n\log{n}=cf(n) af(n/b)=3(n/4)log(n/4)≤(3/4)nlogn=cf(n)。因此满足情况三,从而得到解 T ( n ) = Θ ( n log n ) T(n)=\Theta(n\log{n}) T(n)=Θ(nlogn)。
4.4 补充:递归与分治法
递归的定义:若一个对象部分地包含它自己,或用它自己给自己定义,则称这个对象是递归的;若一个过程直接地或间接地调用自己,则称这个过程是递归的过程。
递归算法的非递归化:
- 利用栈消除递归
- 利用迭代法消除递归
- 末尾递归消除法
4.4.1 Fibonacci数
// 递归
Fibonacci(n)
if n == 0 or n == 1 then
return n
else
return Fibonacci(n-1) + Fibonacci(n-2)
// 非递归
Fibonacci(n)
if n == 0 or n == 1 then
return n
s1 = 0
s2 = 1
for i = 2 to n
sum = s1 + s2
s1 = s2
s2 = sum
return sum
4.4.2 生成全排列
// 递归
fact1(n)
if n == 0
return 1;
else
return n * fact1(n-1)
// 非递归
fact2(n)
p = 1
for i = 1 to n
p = p * i
return p
4.4.3 二分查找
// 递归
BinarySearch(L[ ], x, i, j)
if i > j
return -1
if i == j
if x == L[i]
return i
else
mid = Math.floor((i + j) / 2)
if x == L[mid]
return mid
else if x < L[mid]
return BinarySearch(L[ ], x, i, mid - 1)
else
return BinarySearch(L[ ], x, mid + 1, j)
// 非递归
BinarySearch1(L[ ], n, x) // 找到 x 返回下标,找不到返回-1
left = 1
right = n
flag = 0
while (left <= right and flag == 0)
mid = Math.floor((i + j) / 2)
if x == L[mid]
flag = 1
else if x < L[mid]
right = mid - 1
else
left = mid + 1
if flag == 1
return mid
else
return -1
4.4.4 大整数乘法
(略)
4.4.5 Stranssen矩阵乘法
5 堆排序(ch6)
5.1 堆的概念和存储结构
(二叉)堆可以看作完全二叉树,其存储结构通常是数组。表示堆的数组A中有两个重要属性: A . l e n g t h A.length A.length表示数组元素的个数; A . h e a p − s i z e A.heap-size A.heap−size表示有多少个堆元素在数组中, 0 ≤ A . h e a p − s i z e ≤ A . l e n g t h 0 \le A.heap-size \le A.length 0≤A.heap−size≤A.length。
5.2 堆的性质和种类
假设树的根结点为 A [ 1 ] A[1] A[1],给定一个结点的下标 i i i,可以得到其父结点、左孩子和右孩子的下标:
PARENT(i) return i >> 1
LEFT(i) return i << 1
RIGHT(i) return i << 1 | 1
// 在堆排序好的实现中,这三个函数通常是以宏或者内联函数实现的。
二叉堆的两种形式:大根堆和小根堆
- 大根堆:最大元素在堆顶,除根结点外的所有结点 i i i满足 A [ P A R E N T ( i ) ] ≥ A [ i ] A[PARENT(i)] \ge A[i] A[PARENT(i)]≥A[i]
- 小根堆:最小元素在堆顶,除根结点外的所有结点 i i i满足 A [ P A R E N T ( i ) ] ≤ A [ i ] A[PARENT(i)] \le A[i] A[PARENT(i)]≤A[i]
5.3 堆的操作及其操作时间:建堆、整堆
维护堆的性质(又称整堆):设有数组 A A A和结点 i i i,假定根结点为 L E F T ( i ) LEFT(i) LEFT(i)和 R I G H T ( i ) RIGHT(i) RIGHT(i)的二叉树都是大根堆,但 A [ i ] A[i] A[i]可能小于其孩子结点,因此要对结点 A [ i ] A[i] A[i]进行整堆,使其重新满足大根堆的性质。
上面算法的思想是:找出结点 i i i和左右孩子结点中的最大值,交换结点 i i i和最大值结点,然后递归地向下调用方法,传入下方被交换位置的结点索引。
下图展示了MAX-HEAPIFY
的执行过程。
MAX-HEAPIFY的时间复杂度是 O ( h ) \Omicron(h) O(h), h h h为树高。
我们可以自下而上的调用MAX-HEAPIFY
方法将数组
A
[
1..
n
]
A[1..n]
A[1..n]构建成大根堆。子数组
A
[
⌊
n
2
⌋
+
1..
n
]
A[\lfloor \frac{n}{2} \rfloor + 1..n]
A[⌊2n⌋+1..n]中的元素都是叶结点,每个叶结点都可以看成包含一个元素的堆,只需对
A
[
1..
⌊
n
2
⌋
]
A[1..\lfloor \frac{n}{2} \rfloor]
A[1..⌊2n⌋]的结点进行整堆操作。
我们可以在线性时间 O ( n ) \Omicron(n) O(n)内,把一个无序数组构造称为一个大根堆。
5.4 堆排序算法和时间复杂性
算法思想:初始时,利用BUILD-MAX-HEAP将输入数组
A
[
1..
n
]
A[1..n]
A[1..n]建成大根堆,其中
n
=
A
.
l
e
n
g
t
h
n=A.length
n=A.length。因为数组中的最大元素总在根结点
A
[
1
]
A[1]
A[1]中,通过把它与
A
[
n
]
A[n]
A[n]进行互换,从堆中去掉结点
n
n
n(这一操作可以通过减少
A
.
h
e
a
p
−
s
i
z
e
A.heap-size
A.heap−size的值来实现),在剩余的结点中,因为互换,新的结点可能会违背最大堆的性质,因此需要进行整堆操作,调用MAX-HEAPIFY(A, 1)
,从而在
A
[
1..
n
−
1
]
A[1..n-1]
A[1..n−1]上构造一个新的大根堆。不断重复这一过程,知道堆的大小降到2。
package ch06;
import java.util.Arrays;
public class HeapSort {
private static int heap_size; // 当前堆中的元素
public static int PARENT(int i) {
return ((i + 1) >> 1) - 1;
}
public static int LEFT(int i) {
return ((i + 1) << 1) - 1;
}
public static int RIGHT(int i) {
return ((i + 1) << 1);
}
public static void exchange(int[] arr, int a, int b) { // 交换数组中两个位置的元素
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
public static void MAX_HEAPIFY(int[] A, int i) { // 整堆操作
int l = LEFT(i);
int r = RIGHT(i);
int largest;
if (l + 1 <= heap_size && A[l] > A[i]) {
largest = l;
} else {
largest = i;
}
if (r + 1 <= heap_size && A[r] > A[largest]) {
largest = r;
}
if (largest != i) {
exchange(A, i, largest);
MAX_HEAPIFY(A, largest);
}
}
public static void BUILD_MAX_HEAP(int[] A) { // 建堆操作
heap_size = A.length;
for (int i = A.length / 2 - 1; i >= 0; i--) {
MAX_HEAPIFY(A, i);
}
}
public static void HEAP_SORT(int[] A) { // 堆排序
BUILD_MAX_HEAP(A);
for (int i = A.length - 1; i >= 0; i--) {
exchange(A, 0, i);
heap_size--;
MAX_HEAPIFY(A, 0);
}
}
public static void main(String[] args) {
int[] A = {4, 1, 3, 2, 16, 9, 10, 14, 8, 7};
HEAP_SORT(A);
System.out.println(Arrays.toString(A));
}
}
堆排序的时间复杂度为 O ( n log n ) \Omicron(n \log n) O(nlogn)。
5.5 优先队列及其维护操作
优先队列(priority queue)是一种用来维护由一组元素构成的集合 S S S的数据结构,其中的每个元素都有一个相关的值,称为关键字(key)。一个最大优先队列支持如下操作:
INSERT(S, x)
:把元素 x x x插入集合 S S S中。MAXIMUM(S)
:返回 S S S中具有最大关键字的元素。EXTRACT-MAX(S)
:去掉并返回 S S S中具有最大关键字的元素。INCREASE-KEY(S, x, k)
:将元素 x x x的关键字值增加到 k k k,这里假设 k k k的值不小于 x x x的原关键字值。
最大优先队列的一个应用:基于优先级的共享计算机系统的作业调度。最大优先队列记录将要执行的作业以及他们的优先级。当一个作业完成或者被中断后,调度器调用EXTRACT-MAX(S)
从优先队列中选出最高优先级的作业执行。此外,还可以调用INSERT(S, x)
把一个新作业加入到队列中。
最小优先队列支持的操作如下:
INSERT(S, x)
:把元素 x x x插入集合 S S S中。MINIMUM(S)
:返回 S S S中具有最小关键字的元素。EXTRACT-MIN(S)
:去掉并返回 S S S中具有最小关键字的元素。DECREASE-KEY(S, x, k)
:将元素 x x x的关键字值减小到 k k k,这里假设 k k k的值不大于 x x x的原关键字值。
最小优先队列的一个应用:基于事件驱动的模拟器。发生时间作为关键字,事件必须按照发生的时间顺序进行模拟。
下面是最大优先队列的实现:
MAX-HEAP-MAXIMUM(A)
if A.heap-size < 1
error "heap underflow"
return A[1]
MAX-HEAP-EXTRACT-MAX(A)
max = MAX-HEAP-MAXIMUM(A)
A[1] = A[A.heap-size]
A.heap-size = A.heap-size - 1
MAX-HEAPIFY(A, 1)
return max
MAX-HEAP-INCREASE-KEY(A, x, k)
if k < x.key
error "new key is smaller than current key"
x.key = k
find the index i in array A where object x occurs
while i > 1 and A[PARENT(i)].key < A[i].key
exchange A[i] with A[PARENT(i)], updating the information that maps priority queue objects to array indices
i = PARENT(i)
MAX-HEAP-INSERT(A, x, n)
if A.heap-size == n
error "heap overflow"
A.heap-size = A.heap-size + 1
k = x.key
x.key = -∞
A[A.heap-size] = x
map x to index heap-size in the array
MAX-HEAP-INCREASE-KEY(A, x, k)
下面是MAX-HEAP-INCREASE-KEY的操作过程
在一个包含 n n n个元素的堆中,所有优先队列的操作都可以在 O ( log n ) \Omicron(\log n) O(logn)时间内完成。
6 快速排序(ch7)
6.1 快速排序算法及其最好、最坏时间和平均时间
- 最好时间复杂度: O ( n log n ) \Omicron(n\log n) O(nlogn)。
- 最坏时间复杂度: O ( n 2 ) \Omicron(n^2) O(n2)。
- 平均时间复杂度: O ( n log n ) \Omicron(n\log n) O(nlogn)。
6.2 随机快速排序算法及其期望时间
枢轴(主元)的选择不再像是普通快排那样以 A [ r ] A[r] A[r] 作为主元,而是取 p p p 到 r r r 范围内的随机一个数作为枢轴。其他过程与普通快排相同。
RANDOMIZED-PARTITION(A, p, r)
i = RANDOM(p, r)
exchange A[r] and A[i]
return PARTITION(A, p, r)
RANDOMIZED-QUICKSORT(A, p, r)
if p < r
q = RANDOMIZED-PARTITION(A, p, r)
RANDOMIZED-QUICKSORT(A, p, q - 1)
RANDOMIZED-QUICKSORT(A, q + 1, r)
期望时间: O ( n log n ) \Omicron(n\log n) O(nlogn)。
6.3 Partition算法
对一个典型的子数组 A [ p . . r ] A[p..r] A[p..r]进行快速排序的三步分治过程:
- 分解:数组 A [ p . . r ] A[p..r] A[p..r]被划分为两个(可能为空)的子数组 A [ p . . q − 1 ] A[p..q-1] A[p..q−1]和 A [ q + 1.. r ] A[q+1..r] A[q+1..r],使得 A [ p . . q − 1 ] A[p..q-1] A[p..q−1]中的每一个元素都小于等于 A [ q ] A[q] A[q],而 A [ q + 1.. r ] A[q+1..r] A[q+1..r]中的每个元素都大于等于 A [ q ] A[q] A[q]。返回下标 q q q。
- 解决:通过递归,对子数组 A [ p . . q − 1 ] A[p..q-1] A[p..q−1]和 A [ q + 1.. r ] A[q+1..r] A[q+1..r]调用快速排序。
- 合并:数组 A [ p . . r ] A[p..r] A[p..r]已经有序。
下面的程序实现快速排序:
下图显示了PARTITION(A, p, r)
的操作过程:选择
x
=
A
[
r
]
x=A[r]
x=A[r] 作为枢轴(pivot)(不一定非要选择数组最后一个元素作为枢轴,也可以选择其他元素),并围绕它来划分子数组
A
[
p
.
.
r
]
A[p..r]
A[p..r]。
PARTITION(A, p, r)的核心思想:取数组的最后一个元素为枢轴,使用指针 j j j 从左向右遍历,遇到比枢轴小的元素,移动指针 i i i。这就形成了在从数组开头到指针 j j j 的范围内,从数组开头到指针 i i i 为比枢轴小的元素,从指针 i + 1 i+1 i+1 到指针 j − 1 j-1 j−1 为比枢轴大的元素,直到遍历 A . l e n g t h − 1 A.length-1 A.length−1 个元素。最后,交换指针 i + 1 i+1 i+1 指向的元素和数组最后一个元素即可。
PARTITION(A, p, r)
在操作过程中将待排序的子数组划分为以下几个部分:
- A [ p . . i ] A[p..i] A[p..i]:已经遍历的比枢轴元素小的元素
- A [ i + 1.. j − 1 ] A[i+1..j-1] A[i+1..j−1]:已经遍历的比枢轴元素大的元素
- A [ j . . r − 1 ] A[j..r-1] A[j..r−1]:将要遍历的元素
- A [ r ] A[r] A[r]:枢轴元素
7 线性时间排序(ch8)
8 中位数和顺序统计(ch9)
9 红黑树(ch13)
9.1 红黑树的定义和节点结构
红黑树(如下图a)是满足下面红黑性质的二叉搜索树:
- 每个结点都是红色或黑色
- 根结点是黑色的
- 每个叶结点(NIL)是黑色的
- 如果一个结点是红色,则它的两个子结点都是黑色
- 对每个结点,从该结点到其所有后代结点的简单路径上,均包含相同数目的黑色结点
我们通常将注意力放在红黑树的内部结点,因为它们存储了关键字。
9.2 黑高概念
从某个结点 x x x 出发(不含该结点)到达一个叶结点的任意一条简单路径上的黑色结点个数称为该结点的黑高,记为 b h ( x ) bh(x) bh(x)。红黑树的黑高为根结点的黑高。
9.3 一棵 n n n 个内部结点的红黑树的高度至多是 2 log ( n + 1 ) 2\log(n+1) 2log(n+1)
引理:一棵有 n n n 个内部结点的红黑树的高度最大为 2 log ( n + 1 ) 2\log{(n+1)} 2log(n+1)。
证明:以 x x x 为根结点的子树至少有 2 b h ( x ) − 1 2^{bh(x)}−1 2bh(x)−1 个内部结点。设 h ℎ h 为树高,根据性质4, b h ( x ) ≥ h / 2 bh(x)≥ℎ/2 bh(x)≥h/2 ,于是有 n ≥ 2 h / 2 − 1 n≥2^{ℎ/2}−1 n≥2h/2−1 ,变形可得 h ≤ 2 log ( n + 1 ) ℎ≤2\log(n+1) h≤2log(n+1) ,得证。
9.4 左旋算法
LEFT-ROTATE(T, x)
操作通过改变常数数目的指针,可以将右边两个结点的结构转变为左边的结构。左边的结构可以使用RIGHT-ROTATE(T, y)
转变为右边的结构。
在LEFT-ROTATE(T, x)
的伪代码中,假设
x
.
r
i
g
h
t
≠
T
x.right \ne T
x.right=T 且根结点的父结点为
T
.
n
i
l
T.nil
T.nil。
左旋操作:主要修改三对指针,如下图所示。右旋同理。
下图给出了LEFT-ROTATE
操作修改二叉搜索树的例子。LEFT-ROTATE
和RIGHT-ROTATE
都在
O
(
1
)
\Omicron(1)
O(1) 时间内完成。
9.5 插入算法的时间、至多使用2次旋转
- 插入操作的运行时间为 O ( log n ) \Omicron(\log n) O(logn)。
- 旋转不超过两次。
9.6 删除算法的时间、至多使用3次旋转
- 删除操作的运行时间为 O ( log n ) \Omicron(\log n) O(logn)。
- 旋转不超过3次。
10 数据结构的扩张(ch14)
10.1 动态顺序统计
OS树,又称顺序统计树(Order-Statistic tree),是一棵红黑树在每个结点上扩充一个
s
i
z
e
size
size 属性而得到的。
x
.
s
i
z
e
x.size
x.size 属性,这个属性包含了以
x
x
x 为根的子树(包括
x
x
x 本身)的结点数,即这棵子树的大小。
x
.
s
i
z
e
=
x
.
l
e
f
t
.
s
i
z
e
+
x
.
r
i
g
h
t
.
s
i
z
e
+
1
x.size=x.left.size+x.right.size+1
x.size=x.left.size+x.right.size+1
选择问题及算法
选择问题:在以 x x x 为根的子树中,查找第 i i i 个最小元素。
OS-SELECT(x, i)
r = x.left.size + 1
if i == r
return x // 若i=r,则返回x
elseif i < r
return OS-SELECT(x.left, i) // 若i<r,则递归地在x的左子树中继续寻找第i个元素
else return OS-SELECT(x.right, i-r) // 若i>r,则递归地在x的右子树中继续寻找第i-r个元素
OS-SELECT
的运行时间为
O
(
log
n
)
\Omicron(\log n)
O(logn)。
求秩问题及算法
求秩问题:在OS树中,查找给定结点 x x x 的rank。
OS-RANK(T, x)
r = x.left.size + 1
y = x
while y != T.root
if y == y.p.right
r = r + y.p.left.size + 1
y = y.p
return r
OS-RANK
的运行时间为
O
(
log
n
)
\Omicron(\log n)
O(logn)。
10.2 如何扩张一个数据结构:扩张的步骤
扩充一种数据结构可以分为4个步骤:
- 选择一种基础数据结构。
- 确定基础数据结构中要维护的附加信息。
- 检验基础数据结构上的基本修改操作能否维护附加信息。
- 设计一些新操作。
10.3 区间树的扩张和查找算法
区间树(interval tree)是一种对动态集合进行维护的红黑树,其中每个元素 x x x 都包含一个区间 x . i n t x.int x.int。
在上图的区间树中,每个结点 x x x 包含一个区间,显示在结点中上方;一个以 x x x 为根的子树中所包含的区间端点的最大值显示在结点中下方。这棵树的中序遍历得到按左端点顺序排列的各个结点。
下面按照14.2节中介绍的四个步骤设计区间树:
第一步:基础数据结构(Step1: Underlying data structure)
选择红黑树作为区间树的基础数据结构,每个结点 x x x 包含区间属性 x . i n t x.int x.int,且 x x x 的关键字为 x . i n t . l o w x.int.low x.int.low。因此,该数据结构按中序遍历出的就是按低端点的次序排列的各区间。
第二步:附加信息(Step2: Additional information)
每个结点 x x x 中除了自身区间信息之外,还需要增加一个属性 x . m a x x.max x.max,它是以 x x x 为根的子树中所有区间端点的最大值。
第三步:维护信息(Step3: Maintaining information)
我们必须验证有
n
n
n 个结点区间树上的插入和删除操作能否在
O
(
log
n
)
\Omicron(\log n)
O(logn) 时间内完成。通过给定区间
x
.
i
n
t
x.int
x.int 和结点
x
x
x 的子结点的
m
a
x
max
max值,可以确定
x
.
m
a
x
x.max
x.max值:
x
.
m
a
x
=
m
a
x
{
x
.
i
n
t
.
h
i
g
h
,
x
.
l
e
f
t
.
m
a
x
,
x
.
r
i
g
h
t
.
m
a
x
}
x.max=max\{x.int.high, x.left.max, x.right.max\}
x.max=max{x.int.high,x.left.max,x.right.max}
这样,根据红黑树的扩充定理可得,插入和删除操作的运行时间为
O
(
log
n
)
\Omicron(\log n)
O(logn)。
第四步:设计新操作(Step 4: Developing new operations)
我们只需要增加唯一的新操作INTERVAL-SEARCH(T, i)
,它是用来找出树
T
T
T 中与区间
i
i
i 重叠的那个结点。若树中与
i
i
i 重叠的结点不存在,则下面过程返回指向哨兵
T
.
n
i
l
T.nil
T.nil 的指针。伪代码如下:
INTERVAL-SEARCH(T. i)
x = T.root
while x ≠ T.nil and i does not overlap x.int
if x.left ≠ T.nil and x.left.max ≥ i.low
x = x.left // overlap in left subtree or no overlap in right subtree
else x = x.right // no overlap in left subtree
return x
查找与 i i i 重叠的区间 x x x 的过程从 T . r o o t T.root T.root 开始,逐步向下搜索。当找到一个重叠区间或者 x x x 指向 T . n i l T.nil T.nil 时过程结束。由于基本循环每次迭代耗费 O ( 1 ) \Omicron(1) O(1) 时间,又因为 n n n 个结点的红黑树高度为 O ( log n ) \Omicron(\log n) O(logn),所以INTERVAL-SEARCH的运行时间为 O ( log n ) \Omicron(\log n) O(logn)。
11 动态规划(ch15)
11.1 方法的基本思想和基本步骤
动态规划(dynamic programming)的思想是分治思想和解决冗余。
我们通常按如下4步骤来设计一个动态规划算法:
- 刻画一个最优解的结构特征。
- 递归地定义最优解的值(写出动态规划方程)。
- 计算最优解的值,通常采用自底向上的方法。
- 利用计算出的信息构造一个最优解。
11.2 动态规划和分治法求解问题的区别
- 与分治法相似的是
- 将原问题分解为若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
- 与分治法不同的是
- 经分解的子问题往往不是相互独立的。若用分治法来解,有些共同部分(子问题或子子问题)被重复计算了很多次。
- 而动态规划算法对每个子子问题只求解一次,将其解保存在一个表格中,从而无需每次求解一个子子问题时都需重新计算。
11.3 最优性原理及其问题满足最优性原理的证明方法
Bellman最优性原理:求解问题的一个最优策略序列的子策略序列总是最优的,则称该问题满足最优性原理。
注:对具有最优性原理性质的问题而言,如果有一决策序列包含有非最优的决策子序列,则该决策序列一定不是最优的。
证明方法:反证法。
11.4 算法设计
- 多段图规划
- 矩阵链乘法
- 最大子段和
- 最长公共子序列
12 贪心算法(ch16)
12.1 方法的基本思想和基本步骤
基本思想:从问题的某一个初始解出发,通过一系列的贪心选择----当前状态下的局部最优选择,逐步逼近给定的目标,尽可能的求得全局最优解。在每一步都做出当时看起来是最佳的选择。也就是说,它综述做出局部最优的选择,希望通过局部最优解得到全局最优解。
基本步骤:
从问题的某一初始解出发;
while 依据贪心目标朝给定目标前进一步 do
求出可行解的一个解元素;
由所有解元素组合成问题的一个可行解;
12.2 贪心算法的正确性保证:满足贪心选择性质
贪心选择性质:可通过局部最优(贪心)选择达到全局最优解。
- 通常以自顶向下的方式进行,每次选择后将问题转化为规模更小的子问题。
- 该性质是贪心法使用成功的保障,否则得到的近优解。
12.3 贪心算法与动态规划的比较
动态规划:
- 在每一步中,选择都是根据子问题的解来确定的。
- 先解决子问题
- 自底向上
- 慢,更复杂
贪心算法:
- 在每一步中,我们都会快速地做出一个目前看起来最好的选择。-局部最优(贪心)选择。
- 在解决进一步的子问题之前,可以先做出贪心选择。
- 自顶向下
- 通常较快,简单
12.4 两种背包问题的最优性分析:最优子结构性质和贪心选择性质
两种背包问题:
- 0-1背包
- 小数背包
两种背包都满足最优子结构性质,都可以用动态规划求解。
小数背包还满足贪心选择性质,用贪心算法求解更简单、更快速;但0-1背包问题用贪心算法求解不一定能得到最优解。
12.5 算法设计
- 小数背包
- 活动安排
- 找钱问题
13 回溯法(sch3)
13.1 方法的基本思想和基本步骤
回溯法是一个既带有系统性又带有跳跃性的搜索算法。
- 它在包含问题的所有解的解空间树中,按照深度优先的策略,从根结点出发搜索解空间树。——系统性。
- 算法搜索至解空间树的任一结点时,判断该结点为根的子树是否包含问题的解,如果肯定不包含,则跳过以该结点为根的子树的搜索,逐层向其祖先结点回溯。否则,进入该子树,继续按深度优先的策略进行搜索。——跳跃性。
这种以深度优先的方式系统地搜索问题的解的算法称为回溯法,它适用于解一些组合数较大的问题。
基本步骤:
- 针对问题,定义问题的解空间(对解进行编码);
- 确定易于搜索的解空间组织结构(按树或图组织解);
- 以深度优先搜索方式搜索解空间,搜索过程裁减掉死结点的子树,提高搜索效率。
13.2 回溯法是一种深度遍历的搜索
13.3 术语: 三种搜索空间, 活结点, 死结点, 扩展结点, 开始结点, 终端结点
搜索空间的三种表示:
- 表序表示:搜索对象用线性表数据结构表示;
- 显式图表示:搜索对象在搜索前就用图(树)的数据结构表示;
- 隐式图表示:除了初始结点,其他结点在搜索过程中动态生成.缘于搜索空间大,难以全部存储。
13.4 两种解空间树和相应的算法框架
子集树回溯算法:
Backtrack(int t) // 搜索到树的第t层
{
if t > n // 叶结点是可行解,输出解
output(x);
else
while (all X_t)
{
x[t]=x_t
if (Constraint(t) and Bound(t))
{
Backtrack(t+1)
}
}
}
// 执行时:BackTrack(1)
排列树回溯算法:
Backtrack(int t)//搜索到树的第十层
{//由第+层向第t+1层扩展,确定x[t]的值
if t>n {
output(x);//叶结点是可行解,输出解else
}
for i=t to n
{
swap(x[t],×[i]);
if( Constraint(t) and Bound(t)){}
Backtrack(t+1);
}
swap(x[t],×[i]);
}
}
13.5 算法设计
- 图和树的遍历
- n皇后问题
- 0-1背包
- 排列生成问题
- TSP问题
14 平摊分析(ch17)
15 二项堆(ch19 in textbook version 2)
15.1 为什么需要二项堆?二项堆和二叉堆上的几个基本操作时间复杂性
二叉堆合并操作相当于重建堆,开销大。
过程 | 二叉堆 | 二项堆 |
---|---|---|
MAKE-HEAP | Θ ( 1 ) \Theta(1) Θ(1) | Θ ( 1 ) \Theta(1) Θ(1) |
INSERT | Θ ( log n ) \Theta(\log n) Θ(logn) | Ω ( log n ) \Omega(\log n) Ω(logn) |
MINIMUM | Θ ( 1 ) \Theta(1) Θ(1) | Ω ( log n ) \Omega(\log n) Ω(logn) |
EXTRACT-MIN | Θ ( log n ) \Theta(\log n) Θ(logn) | Θ ( log n ) \Theta(\log n) Θ(logn) |
UNION | Θ ( n ) \Theta(n) Θ(n) | Ω ( log n ) \Omega(\log n) Ω(logn) |
DECREASE-KEY | Θ ( log n ) \Theta(\log n) Θ(logn) | Θ ( log n ) \Theta(\log n) Θ(logn) |
DELETE | Θ ( log n ) \Theta(\log n) Θ(logn) | Θ ( log n ) \Theta(\log n) Θ(logn) |
15.2 二项堆定义和存储结构
二项堆 H H H 由一组满足下面二项堆性质的二项树组成。
- H H H 中的每个二项树遵循最小堆性质,即结点的关键字大于或等于其父结点的关键字。我们称这种树是最小堆有序的。
- 对任意非负整数 k k k,在 H H H 中至多有一棵二项树的根具有度数 k k k。
二项堆 H H H 最多包含 ⌊ log n ⌋ + 1 \lfloor \log n \rfloor+1 ⌊logn⌋+1 棵二项树。
上图为一个包含13个结点的二项堆。(a)一个二项堆包含了二项树 B 0 , B 2 , B 3 B_0, B_2, B_3 B0,B2,B3,它们分别由1个、4个、8个结点,即共有13个结点。由于每棵二项树都是最小堆有序的,所以任意结点的关键字都不小于其父结点的关键字。图中还标记出了根链,它是一个按根的度数递增排序的链表。(b)二项堆 H H H 的一个更具体的表示,每棵二项树按左孩子、右兄弟表示方式存储,每个结点还存储自身的度数。
15.3 二项堆上合并操作及过程
下面的过程合并二项堆
H
1
H_1
H1 和
H
2
H_2
H2,并返回结果堆。在合并过程中也破坏了
H
1
H_1
H1 和
H
2
H_2
H2。过程中使用了一个辅助过程BINOMIAL-HEAP-MERGE
,将
H
1
H_1
H1 和
H
2
H_2
H2 的根链表合并成一个按度数单调递增的链表。BINOMIAL-HEAP-MERGE
过程与归并排序的过程类似。
BINOMIAL-HEAP-UNION的运行时间为 O ( log n ) \Omicron(\log n) O(logn)。
15.4 二项堆应用(尤其是在哪些图论算法上有应用)
- 最小生成树算法
- Kruskal算法
- Prim算法
- 最短路径算法
- Dijkstra算法
16 不相交集数据结构(ch31)
17 图论算法(ch32-ch35)
17.1 BFS和DFS算法
17.1.1 白色、灰色和黑色结点概念和作用
白色顶点表示该顶点未被发现,灰色顶点表示其邻接顶点可能还有未发现顶点,黑色顶点表示其邻接顶点全部被发现。
17.1.2 计算过程及其时间复杂度
如果使用邻接链表存储,时间复杂度为 O ( ∣ V ∣ + ∣ E ∣ ) \Omicron(|V|+|E|) O(∣V∣+∣E∣)。
如果使用邻接矩阵存储,时间复杂度为 O ( ∣ V ∣ 2 ) \Omicron(|V|^2) O(∣V∣2)。