伪码:
INSERTION-SORT(A)
for j <- 2 to length[A]
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
C代码:(C的数组下标从0开始,而伪码中从1开始)
void insertion_sort(int *arr, size_t size)
{
int i, j, key;
assert(NULL != arr);
for (j = 1; j < size; ++j) {
key = arr[j];
for (i = j - 1; i >= 0 && arr[i] > key; --i)
arr[i+1] = arr[i];
arr[i+1] = key;
}
}
正确性分析:
说明:#include
#include
#include
#include
void insertion_sort(int *arr, size_t size)
{
int key;
int i, j;
assert(NULL != arr);
for (j = 1; j < size; ++j) {
key = arr[j];
for (i = j - 1; i >= 0 && arr[i] > key; --i)
arr[i+1] = arr[i];
arr[i+1] = key;
}
}
void print_arr(const int *arr, size_t size, const char *info)
{
int i;
assert(NULL != arr);
printf("%s: ", info);
for (i = 0; i < size; ++i)
printf("%d ", arr[i]);
printf("\n");
}
void init_arr(int *arr, size_t size)
{
int i;
assert(NULL != arr);
srand((unsigned int)time(NULL));
for (i = 0; i < size; ++i)
arr[i] = rand()%100;
}
int main()
{
int arr[10];
init_arr(arr, 10);
print_arr(arr, 10, "before");
insertion_sort(arr, 10);
print_arr(arr, 10, "after");
return 0;
}
习题:
2.1-1 以图2-2为模型,说明INSERTION-SORT在数组A=<31,41,59,26,41,58>上的执行过程。 31, 41(j), 59, 26, 41, 58
31(j), 41, 59(j), 26, 41, 58
26, 31, 41, 59(j), 41, 58
26, 31, 41, 41, 59(j), 58
26, 31, 41, 41, 58, 59(j)
标注为x(j)的元素表示此时正在以j执行循环。
int find_linear(const int *arr, size_t size, int v)
{
int i;
for (i = 0; i < size && arr[i] != v; ++i);
if (i == size) i = -1; /* -1 means NIL */
return (int)i;
}
位 (n+1) (n) (n-1) ... (2) (1)
A A[0] A[1] ... A[n-1] A[n]
B B[0] B[1] ... B[n-1] B[n]
C C[0] C[1] C[2] ... C[n] C[n+1]
伪码:
BINARY-SUM(A, B, C, n)
for i <- 1 to n+1
do C[i] <- 0
for i <- n to 1
do if A[i] + B[i] + C[i+1] >= 2
then C[i] <- 1
C[i+1] <- (C[i+1] + A[i] + B[i])%2
C代码:
void binary_sum(const int *arr_a, const int *arr_b, int *arr_c, size_t n)
{
int i;
assert(NULL != arr_a && NULL != arr_b && NULL != arr_c);
for (i = 0; i < n+1; ++i)
arr_c[i] = 0;
if (0 == n) return;
for (i = n-1; i >= 0; --i) {
if (arr_a[i] + arr_b[i] + arr_c[i+1] >= 2)
arr_c[i] = 1;
arr_c[i+1] = (arr_a[i] + arr_b[i] + arr_c[i+1])%2;
}
}
循环结束时,A和B中的所有位已经处理过了,如果最高为有进位,则存储在C[1]中,否则C[1]保持为0,因此结果是正确的。
2.2 算法分析
算法分析:
预测算法运行所需资源。
资源通常是指算法运行时间。
实际中需要考虑的其他资源包括:内存、通信带宽、硬件等。
实现模型:
通用处理器;
随机存取RAM;
均等的指令成本,例如当k较小时(小于等于处理器位宽),2^k可认为在常数时间内完成(移位操作)。
插入排序分析:
T(n) = Θ(n^2)
最坏/好、平均/期望算法运行时间:
最坏/好:算法上/下界(阈值)
平均/期望:随机化分析结果
增长指数:
只统计增长最快项,因为当n增大时,增长最快项是关键。
实际项目中,还需要考虑输入规模和实现的复杂度、可维护性等。
衡量方法Θ
习题:
2.2-1 用Θ形式表示函数n^3/1000-100n^2-100n+3。
Θ(n^3)
2.2-2 考虑对数组A中的n个数进行排序的问题:首先找出A中的最小元素,并将其与A[1]中元素进行交换。接着,找出A中的次最小元素,并将其与A[2]中的元素进行交换。对A中头n-1个元素继续这一过程。写出这个算法的伪代码,该算法称为选择排序(selection)。对这个算法来说,循环不变式是什么?为什么它仅需要在头n-1个元素上运行,而不是在所有n个元素上运行?以Θ形式写出选择排序的最佳和最坏情况下的运行时间。
伪码:
SELECTION-SORT(A)
for i <- 1 to length[A]-1
do min <- i
for j <- i+1 to length[A]
do if A[j] < A[min]
then min <-j
exchange(A[i], A[min])
C代码:
void selection_sort(int *arr, size_t size)
{
int i, j, min;
assert(NULL != arr);
for (i = 0; i < size; ++i) {
min = i;
for (j = i + 1; j < size; ++j) {
if (arr[j] < arr[min])
min = j;
}
/* exchanges */
if (min != i) {
arr[i] = arr[i]^arr[min];
arr[min] = arr[min]^arr[i];
arr[i] = arr[i]^arr[min];
}
}
}
循环不变式:
2.3 算法设计
2.3.1 分治法(Divede-and-Conquer)
递归的把原问题划分成n个较小规模的结构与原问题相似的子问题,递归地解决这些子问题,然后合并结果,就得到原问题的解。
分治模式在每一层上都有三个步骤:分解、解决、合并。
合并排序完全按照上述模式操作,如下:
分解:将n个元素分成各含n/2个问题的子序列。
解决:用合并排序法对子序列递归地排序。
合并:合并两个已排序的子序列以得到排序结果。
在对子序列排序时,其长度为1时认为是已排序的,递归结束。
合并过程如下:
A为待排序数组,n=r-p+1是需要合并的子序列。用∞作为哨兵,它大于任意元素。
伪码:
MERGE(A, p, q, r)
n1 <- q - p +1
n2 <- r - q
create arrays L[1..n1+1] and R[1..n2+1]
for j <- 1 to n1
do L[i] <- A[p+i-1]
for j <- 1 to n2
do R[j] <- A[q+j]
L[n1+1] <- ∞
R[n2+1] <- ∞
i <- 1
j <- 1
for k <- p to r
do if L[i] ≤ R[j]
then A[k] <- L[i]
i <- i + 1
else A[j] <- R[j]
j <- j + 1
MERGE-SORT(A, p, r)
if p < r
then q <- ⌊(p+r)/2⌋
MERGE-SORT(A, p, q)
MERGE-SORT(A, a+1, r)
MERGE(A, p, q, r)
C代码:
int merge(int *arr, size_t p, size_t q, size_t r)
{
int i, j, k, n1 = q - p + 1, n2 = r - q, *arr_l, *arr_r;
assert(NULL != arr);
assert(p <= q && q < r);
arr_l = malloc(sizeof(int)*n1);
arr_r = malloc(sizeof(int)*n2);
if (NULL == arr_l || NULL == arr_r) {
/* to free a null pointer is a safe operation */
free(arr_l);
free(arr_r);
return -ENOMEM;
}
/* we don't use dummy node but to check l and r sub-array */
for (i = 0; i < n1; ++i)
arr_l[i] = arr[p+i];
for (j = 0; j < n2; ++j)
arr_r[j] = arr[q+j+1];
for (i = 0, j = 0, k = p; i < n1 && j < n2; ++k) {
if (arr_l[i] < arr_r[j]) {
arr[k] = arr_l[i];
++i;
} else {
arr[k] = arr_r[j];
++j;
}
}
/* append unused nodes */
while (i < n1) arr[k++] = arr_l[i++];
while (j < n2) arr[k++] = arr_r[j++];
free(arr_l);
free(arr_r);
return 0;
}
int merge_sort(int *arr, size_t p, size_t r)
{
if (p < r) {
size_t q = (p+r)/2;
if (0 == merge_sort(arr, p ,q) && 0 == merge_sort(arr, q+1, r))
return merge(arr, p, q, r);
return -ENOMEM;
}
return 0;
}
-----------------------------
练习
2.3-1 以图2-4为模型,说明合并排序在输入数组A=〈3,41,52,26,38,57,9,49〉上的执行过程。
^ ^
| 3 9 26 38 49 51 52 57 |
| / \ |
| 3 26 51 52 9 38 49 57 |
| / \ / \ |
| 3 41 26 52 38 57 9 49 |
| / \ / \ / \ / \ |
| 3 41 52 26 38 57 9 49 |
MERGEA(A, p, q, r)
n1 <- q - p +1
n2 <- r - q
create arrays L[1..n1] and R[1..n2]
for j <- 1 to n1
do L[i] <- A[p+i-1]
for j <- 1 to n2
do R[j] <- A[q+j]
i <- 1
j <- 1
k <- p
while i ≤ n1 and j ≤ n2
do if L[i] ≤ R[j]
then A[k] <- L[i]
i <- i + 1
else A[j] <- R[j]
j <- j + 1
k <- k + 1
if i ≤ n1
then while i ≤ n1
do A[k] <- L[i]
k <- k + 1
i <- i + 1
else while j ≤ n2
do A[k] <- R[j]
k <- k + 1
j <- j + 1
|-- 2 n=2时
T(n) = |
|-- 2T(n/2) + n 如果n=2^k, k>1
的解为 T(n) = nlgn
INSERTION-SORT-RECURSIVE(A, n, k)
if k > 1
then INSERTION-SORT-RECURSIVE(A, n, k-1)
key <- A[k]
for i <- k-1 to 1
do if A[i] > key
then A[i+1] <- A[i]
A[i+1] <- key
BINARY-SEARCH-ITERATE(A, v)
low <- 1
high <- length[A]
while low ≤ high
do mid <- ⌊(low + high)/2⌋
if A[mid] = v
then return mid
elif A[mid] < v
then low <- mid + 1
else high <- mid - 1
return NIL
注意,当查找到时,设置low=high+1,因此下次循环检测时循环结束,并且idx已被设置为正确值。
递归版本:
BINARY-SEARCH-RECURSIVE(A, v, low, high)
if low < high
then return NIL
mid <- ⌊(low + high)/2⌋
if A[mid] = v
then return mid
elif A[mid] < v
then return BINARY-SEARCH-RECURSIVE(A, v, mid+1, high)
else return BINARY-SEARCH-RECURSIVE(A, v, low, mid-1)
可知,二分查找时间复杂度满足
SEARCH-SUM(S, x)
MERGE-SORT(S, 0, length[S])
for i <- 0 to length[S]
do v <- x - S[i]
if BINARY-SEARCH-ITERATE(S, v) != NIL
then return true
return false
分析:
------------------------
思考题
2-1在合并排序中对小数组采用插入排序
尽管合并排序的最坏情况运行时间为Θ(nlgn),插入排序的最坏情况运行时间为Θ(n^2),但插入排序中的常数因子使得它在n较小时,运行得要更快一些。因此,在合并排序算法中,当子问题足够小时,采用插入排序就比较合适了。考虑对合并排序做这样的修改,即采用插入排序策略,对n/k个长度为k的子列表进行排序,然后,再用标准的合并机制将它们合并起来,此处k是一个待定的值。
a)证明在最坏情况下,n/k个子列表(每一个子列表的长度为k)可以用插入排序在Θ(nk)时间内完成排序。
b)证明这些子列表可以在Θ(nlg(n/k))最坏情况时间内完成合并。
c)如果已知修改后的合并排序算法的最坏情况运行时间为Θ(nk+nlg(n/k)),要使修改后的算法具有与标准合并排序算法一样的渐近运行时间,k的最大渐近值(即Θ形式)是什么(以n的函数形式表示)?
d)在实践中,k的值应该如何选取?
解答:
a) 每个子列表长度为k,因此每个子列表插入排序的时间为Θ(k^2)。共有n/k个子列表,故总的时间为Θ(k^2)*(n/k) = Θ(nk)
b) 此时共需合并n/k次(因为分解了n/k次),每次合并的时间为Θ(n),故所有合并的时间为Θ(nlg(n/k))
c) 问题等价于求解 Θ(nk + nlg(n/k)) = Θ(nlgn)。其最大渐近值为lgn。
d) 这是个实验问题,应该在k的合法范围内测试可能的k,用T-INSERTION-SORT(k)表示k个元素的插入排序时间,T-MERGE-SORT(k)表示k个元素的合并排序时间。该问题等价于测试求解T-INSERTION-SORT(k)/T-MERGE-SORT(k)比值最小的k值。
2-2冒泡排序算法的正确性
冒泡排序(bubblesort)算法是一种流行的排序算法,它重复地交换相邻的两个反序元素。
BUBBLESORT(A)
1 for i <- 1 to length[A]
2 do for j <- length[A] downto i + 1
3 do if A[j] < A[j-1]
4 then exchange A[j] <-> A[j-1]
a) 设A′表示BUBBLESORT(A)的输出。为了证明BUBBLESORT是正确的,需要证明它能够终止,并且有:
A'[1] ≤ A'[2] ≤ ... ≤ A'[n] (2.3)
其中n=length[A]。为了证明BUBBLESORT的确能实现排序的效果,还需要证明什么?
下面两个部分将证明不等式(2.3)。
b)对第2~4行中的for循环,给出一个准确的循环不变式,并证明该循环不变式是成立的。在证明中应采用本章中给出的循环不变式证明结构。
c)利用在b)部分中证明的循环不变式的终止条件,为第1~4行中的for循环给出一个循环不变式,它可以用来证明不等式(2.3)。你的证明应采用本章中给出的循环不变式的证明结构。
d)冒泡排序算法的最坏情况运行时间是什么?比较它与插入排序的运行时间。
解答:
a) A中所有的元素都在A'中,或者说A中的任意元素在A'中存在且唯一(一一对应)。
b) 冒泡排序中,需要证明子数组A[j-1..n]的最小元素为A[j-1]。 初始、保持、终止三元素描述如下:
初始:
j=n,子数组为A[j-1..n]=A[n-1..n]有两个元素。在循环内部,通过条件交换语句,可以保证A[n-1] < A[n]成立。因此A[j-1]是A[j-1..n]中的最小元素。
保持:
每次迭代开始时,A[j]是A[j..n]中的最小元素。
在迭代操作中,当A[j] < A[j-1]时交换,因此总有A[j-1] < A[j]。
可知,本次迭代操作完成后,A[j-1]一定是A[j-1..n]中的最小元素。
终止:
j=i+1时退出,因此结束时,A[i]一定是A[i..n]中的最小元素。
c) 描述如下:
初始:
i=1,是A中的第一个元素,因此内部循环完成后,可以保证A[1]中保存A[1..n]的最小元素。
保持:
每次递增i时,执行内部循环,因此A[i]中保存A[i..n]中的最小元素。
可知每次内部循环完成后,都有 A[1] ≤ A[2] ≤ ... ≤ A[i]
终止:
i=length[A]时终止,此时有 A[1] ≤ A[2] ≤ ... ≤ A[n]。
d)
冒泡排序最坏和最好运行时间均为Θ(n^2);
插入排序的最坏运行时间为Θ(n^2),但是最好运行时间为Θ(n);
排序前A所有元素已经有序时,插入排序达到最好运行时间。
2.3 霍纳规则的正确性
以下的代码片段实现了用于计算多项式
P(x) = a0 + x(a1 + x(a2 + ... + x(an-1 + xan)...))
的霍纳规则(Horner's rule)。
给定系数a0, a1, ..., an-1, an以及x的值,有:
y <- 0
i <- n
while i ≥ 0
do y <- ai + x*y
i <- i - 1
a)这一段实现霍纳规则的代码的渐近运行时间是什么?
NATIVE-POLYNOMIAL-EVALUATION(A, x)
y <- 0
for i <- 0 to length[A]
v <- 1
do for j <- 1 to i
do v <- v*x
y <- y + A[i]*v
return y
算法运行时间为Θ(n^2)。而霍纳规则代码段时间复杂度为Θ(n)。因此,该算法的时间耗费是霍纳规则代码段的Θ(n)倍。