2.1 插入排序
插入排序解决的问题:
输入:n个数构成的序列<a1, a2, ..., an>
输出:排序输入序列<a1, a2, ..., an>为<a1', a2', ..., an'>,满足a1' ≤ a2' ≤ ... ≤ an'
插入算法在执行循环之前,A[1,j-1]是已排好序的,而每次循环都保持这个情况,因此当j从2到length[A]循环完成后,整个数组已排序。
初始:
j=2,A[1,j-1]只包含一个元素A[1],因此是已排序的;
保持:
对于任意j(2 ≤ j ≤ length[A]),保存A[j]到key
内部循环总是从A[j-1], A[j-2], ..., A[1]序列中依次查找所有大于A[j]的值并依次后移。
当循环结束时,A[i+1,j]中存放所有大于key的值,而A[1,i-1]中存放所有不大于key的值,且A[i]是空闲的。
存放key到A[i]。
因此每次循环后,A[1,j]是有序的。
退出:
j > length[A]时循环结束,此时A[1,length[A]]全部有序。
数学归纳法证明:
当j=2时,A[1,j-1]只包含一个元素A[1],因此肯定是有序的。
假定对于任意j=m(2 ≤ m < length[A]),循环完成后有A[1,j]有序
则当j=m+1时:
A[1,m]肯定有序。
保存A[m+1]到key。
内层循环结束时,A[1,i]子数组中所有元素不大于key,而A[i+2,m+1]子数组中所有元素大于key,A[i+1]不持有有效数据。
保存key到A[i+1]。
综上可知:A[1,m+1]此时是有序的。
因此对于所有的j(2 ≤ j ≤ length[A]),在任意时刻,有A[1,j]有序。
当循环迭代到j=length[A]算法结束时,数组A已排序。
测试程序:
可以通过如下程序测试插入排序算法的正确性。其中辅助函数print_arr、init_arr可能在以后多次用到:
如下:
2.1-2 重写过程INSERTION-SORT,使之按非升序(而不是按非降序)排序。
只需要更改一行:
更改
while i > 0 and A[i] > key
为
while i > 0 and A[i] < key
即可。
2.1-3 考虑下面的查找问题:
输入:一列数A=<a1,a2,…,an>和一个值V。
输出:下标i,使得V=A[i],或者当V不再A中出现时为NIL。
写出针对这个问题的线性查找的伪代码,它顺序地扫描整个序列以查找V。利用循环不变式证明算法的正确性。确保所给出的循环不变式满足三个必要的性质。
FIND-LINEAR(A, v, i)
i <- NIL
j <- 1
while j ≤ length[A] and A[j] != v
do j <- j + 1
if j ≤ length[A]
then i <- NIL
证明:
初始:
i=NIL,初始化为未查找到。
保持:
从j=1到length[A],依次判断,如果A[j]等于v,则结束循环。
退出:
循环结束时,判断j是否有效
如果j在有效范围(小于等于length[A]),则证明查找成功,因此设置i为正确的索引。
否则,循环因为越界结束,v没有找到,i依然为NIL。
2.1-4 有两个各存放在数组A和B中的n位二进制整数,考虑它们的相加问题。两个整数的和以二进制形式存放在具有(n+1)个元素的数组C中。请给出这个问题的形式化描述,并写出伪代码。
存储说明:最高位存在数组的第一个元素中,最低位存在数组的最后一个元素中。
因为A/B各有n个元素而C有n+1个元素,因此对于任意i(1 ≤ i ≤ n),A[i]、B[i]和C[i+1]总在相同位(权数)。如下:
插入排序解决的问题:
输入:n个数构成的序列<a1, a2, ..., an>
输出:排序输入序列<a1, a2, ..., an>为<a1', a2', ..., an'>,满足a1' ≤ a2' ≤ ... ≤ an'
伪码:
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;
}
}
正确性分析:
说明:插入算法在执行循环之前,A[1,j-1]是已排好序的,而每次循环都保持这个情况,因此当j从2到length[A]循环完成后,整个数组已排序。
初始:
j=2,A[1,j-1]只包含一个元素A[1],因此是已排序的;
保持:
对于任意j(2 ≤ j ≤ length[A]),保存A[j]到key
内部循环总是从A[j-1], A[j-2], ..., A[1]序列中依次查找所有大于A[j]的值并依次后移。
当循环结束时,A[i+1,j]中存放所有大于key的值,而A[1,i-1]中存放所有不大于key的值,且A[i]是空闲的。
存放key到A[i]。
因此每次循环后,A[1,j]是有序的。
退出:
j > length[A]时循环结束,此时A[1,length[A]]全部有序。
数学归纳法证明:
当j=2时,A[1,j-1]只包含一个元素A[1],因此肯定是有序的。
假定对于任意j=m(2 ≤ m < length[A]),循环完成后有A[1,j]有序
则当j=m+1时:
A[1,m]肯定有序。
保存A[m+1]到key。
内层循环结束时,A[1,i]子数组中所有元素不大于key,而A[i+2,m+1]子数组中所有元素大于key,A[i+1]不持有有效数据。
保存key到A[i+1]。
综上可知:A[1,m+1]此时是有序的。
因此对于所有的j(2 ≤ j ≤ length[A]),在任意时刻,有A[1,j]有序。
当循环迭代到j=length[A]算法结束时,数组A已排序。
测试程序:
可以通过如下程序测试插入排序算法的正确性。其中辅助函数print_arr、init_arr可能在以后多次用到:
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
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执行循环。
2.1-2 重写过程INSERTION-SORT,使之按非升序(而不是按非降序)排序。
只需要更改一行:
更改
while i > 0 and A[i] > key
为
while i > 0 and A[i] < key
即可。
2.1-3 考虑下面的查找问题:
输入:一列数A=<a1,a2,…,an>和一个值V。
输出:下标i,使得V=A[i],或者当V不再A中出现时为NIL。
写出针对这个问题的线性查找的伪代码,它顺序地扫描整个序列以查找V。利用循环不变式证明算法的正确性。确保所给出的循环不变式满足三个必要的性质。
FIND-LINEAR(A, v, i)
i <- NIL
j <- 1
while j ≤ length[A] and A[j] != v
do j <- j + 1
if j ≤ length[A]
then i <- NIL
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;
}
证明:
初始:
i=NIL,初始化为未查找到。
保持:
从j=1到length[A],依次判断,如果A[j]等于v,则结束循环。
退出:
循环结束时,判断j是否有效
如果j在有效范围(小于等于length[A]),则证明查找成功,因此设置i为正确的索引。
否则,循环因为越界结束,v没有找到,i依然为NIL。
2.1-4 有两个各存放在数组A和B中的n位二进制整数,考虑它们的相加问题。两个整数的和以二进制形式存放在具有(n+1)个元素的数组C中。请给出这个问题的形式化描述,并写出伪代码。
存储说明:最高位存在数组的第一个元素中,最低位存在数组的最后一个元素中。
因为A/B各有n个元素而C有n+1个元素,因此对于任意i(1 ≤ i ≤ n),A[i]、B[i]和C[i+1]总在相同位(权数)。如下: