通俗地说,算法是任何明确定义的计算过程,它将一些值或一组值作为输入,并在有限的时间内产生一些值或一组值作为输出。因此,算法是将输入转换为输出的一系列计算步骤。
算法的特征:输入、输出、确定性、有限性、正确性、通用性。
时间复杂性和空间复杂性
最坏、最好和平均情形复杂性
时间复杂度 O ( n log n ) \Omicron(n \log n) O(nlogn)。
对于给定的函数 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)
对于给定的函数 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)
对于给定的函数 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))
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)
代入法求解递归式分两步:
例题:求解 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
在递归树中,每个结点表示一个单一子问题的代价,子问题对应某次递归函数的调用。我们将树中每层中的代价求和,得到每层代价,然后将所有层的代价求和,得到所有层次递归调用的总代价。
递归式最适合用来生成好的猜测,然后即可用代入法来验证猜测是否正确。
例如, 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。得证。
假设有递推式 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)有如下渐近界:
例如, 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)。
递归的定义:若一个对象部分地包含它自己,或用它自己给自己定义,则称这个对象是递归的;若一个过程直接地或间接地调用自己,则称这个过程是递归的过程。
递归算法的非递归化:
// 递归
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
// 递归
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
// 递归
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
(略)
(二叉)堆可以看作完全二叉树,其存储结构通常是数组。表示堆的数组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。
假设树的根结点为 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]
维护堆的性质(又称整堆):设有数组 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)内,把一个无序数组构造称为一个大根堆。
算法思想:初始时,利用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)。
优先队列(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)时间内完成。
枢轴(主元)的选择不再像是普通快排那样以 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)。
对一个典型的子数组 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)是满足下面红黑性质的二叉搜索树:
我们通常将注意力放在红黑树的内部结点,因为它们存储了关键字。
从某个结点 x x x 出发(不含该结点)到达一个叶结点的任意一条简单路径上的黑色结点个数称为该结点的黑高,记为 b h ( x ) bh(x) bh(x)。红黑树的黑高为根结点的黑高。
引理:一棵有 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) ,得证。
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) 时间内完成。
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
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)。
扩充一种数据结构可以分为4个步骤:
区间树(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)。
动态规划(dynamic programming)的思想是分治思想和解决冗余。
我们通常按如下4步骤来设计一个动态规划算法:
Bellman最优性原理:求解问题的一个最优策略序列的子策略序列总是最优的,则称该问题满足最优性原理。
注:对具有最优性原理性质的问题而言,如果有一决策序列包含有非最优的决策子序列,则该决策序列一定不是最优的。
证明方法:反证法。
基本思想:从问题的某一个初始解出发,通过一系列的贪心选择----当前状态下的局部最优选择,逐步逼近给定的目标,尽可能的求得全局最优解。在每一步都做出当时看起来是最佳的选择。也就是说,它综述做出局部最优的选择,希望通过局部最优解得到全局最优解。
基本步骤:
从问题的某一初始解出发;
while 依据贪心目标朝给定目标前进一步 do
求出可行解的一个解元素;
由所有解元素组合成问题的一个可行解;
贪心选择性质:可通过局部最优(贪心)选择达到全局最优解。
动态规划:
贪心算法:
两种背包问题:
两种背包都满足最优子结构性质,都可以用动态规划求解。
小数背包还满足贪心选择性质,用贪心算法求解更简单、更快速;但0-1背包问题用贪心算法求解不一定能得到最优解。
回溯法是一个既带有系统性又带有跳跃性的搜索算法。
这种以深度优先的方式系统地搜索问题的解的算法称为回溯法,它适用于解一些组合数较大的问题。
基本步骤:
搜索空间的三种表示:
子集树回溯算法:
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]);
}
}
二叉堆合并操作相当于重建堆,开销大。
过程 | 二叉堆 | 二项堆 |
---|---|---|
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) |
二项堆 H H H 由一组满足下面二项堆性质的二项树组成。
二项堆 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 的一个更具体的表示,每棵二项树按左孩子、右兄弟表示方式存储,每个结点还存储自身的度数。
下面的过程合并二项堆 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)。
白色顶点表示该顶点未被发现,灰色顶点表示其邻接顶点可能还有未发现顶点,黑色顶点表示其邻接顶点全部被发现。
如果使用邻接链表存储,时间复杂度为 O ( ∣ V ∣ + ∣ E ∣ ) \Omicron(|V|+|E|) O(∣V∣+∣E∣)。
如果使用邻接矩阵存储,时间复杂度为 O ( ∣ V ∣ 2 ) \Omicron(|V|^2) O(∣V∣2)。