2016.11.30开始,每周两章。
2.1-1 以图2-2为模型,说明INSERTION-SORT在数组A = {31,41,59,26,41,58}上的执行过程
31,41,59
26,31,41,59 循环* 3
26,31,41,41,59 循环 * 1
26,31,41,41,58,59 循环* 1
2.1-2 重写过程INSERTION-SORT,使之按非升序(而不是非降序)排序
for j = 2 to A.length
{
key = A[j];
i = j - 1; //被比较数
for( i > 0 and A[i] < key )
{
A[i + 1] = A[i];
i = i - 1;
}
A[i + 1] = key;
}
区别仅仅在于内置for循环的大于号和小于号,排序均是从左到右插入排序,判断符号决定插入的位置
2.1-3 考虑以下查找问题:
输入: n个数的一个序列A = 和一个值v
输出:下标i使得v = A[i]或者当v不在A中出现时,v为特殊值NIL
写出线性查找的伪代码,它扫描整个序列来查找v。使用一个循环不变式来证明你的算法是正确的。确保你的循环不变式满足三条必要的性质。
for i = 1 to A.length
{
if( i > A.length )
return NIL;
if( v = A[i] )
return i;
}
2.1-4 考虑把两个n位二进制整数加起来的问题,这两个整数分别存储在两个n元数组A和B中。这两个整数的和应按二进制形式存储在一个(n+1)元数组C中。请给出该问题的形式化描述,并写出伪代码。
从右到左不断取ABC数组中的数字,进行判断,加入新数组D中,判断过程有8种情况,其中新数组C是用来存放进位的全零数组
for( i = A.length downto 0)
{
if( i = 0 )
D[i] = C[i];
else
{
if( A[i] = 0, B[i] = 0, C[i] = 0)
D[i] = 0;
else if( 001 )
D[i] = 1;
else if( 010 )
D[i] = 1;
else if( 011 )
{
D[i] = 0;
C[i - 1] = 1;
}
else if( 100 )
D[i] = 1;
else if( 101 )
{
D[i] = 0;
C[i - 1] = 1;
}
else if( 110 )
{
D[i] = 0;
C[i - 1] = 1;
}
else if( 111 )
{
D[i] = 1;
C[i - 1] = 1;
}
}
}
2.2-1 用theta记号表示函数 n3/1000 - 100n² - 100n + 3.
theta n3
2.2-2 考虑排序存储在数组A中的n个数:首先找出A中的最小元素并将其与A【1】中的元素进行交换。接着,找出A中的次最小元素并将其与A【2】中的元素进行交换。对A的前n - 1个元素按该方式继续。该算法称为选择算法,写出其伪代码。该算法维持的循环不变式是什么?为什么他只要对前n - 1个元素,而不是对所有n个元素运行?用theta记号给出选择排序的最好情况与最坏情况的运行时间。
第一遍 n -1次比较
for j = 1 to A.length //c1 n + 1
{
//min = A[j];
flag = j; //c2 n
for( i = j + 1 to A.length ) //c3 n/2 * (n + 1)
{
if( A[j] > A[i] ) //c4 (α - 1) * (n + 1)
//min = A[i];
flag = i; //c5 α/2 * (n + 1)
} //now I find the min
temp = A[j]; //c6 n
A[j] = A[flag]; //c7 n
A[flag] = temp;//swap //c8 n
}
循环不变式:
初始化:初始数组为空 成立
保持:每次放入的都是剩下的最小的元素 成立
终止:归纳
因为最后一个比所有其他元素都大 所以已经被置换到最后
运行时间T(n) = c1 * (n + 1) + c2 * n + c3 * n /2 * (n + 1) + c4 * (α - 1) * (n + 1) + c5 * α/2 * (n + 1) + c6 * n + c7 * n + c8 * n
theta n²
2.2-3 再次考虑线性查找问题。假定要查找的元素等可能的为数组中的任何元素,平均需要检查输入序列的多少元素?最坏情况又如何呢?用theta记号给出线性查找的平均情况和最坏情况运行时间。证明你的答案。
平均需要检查输入序列的一半。最坏情况即为检查所有输入的元素。
theta2/n
theta n
因为输入等可能的为数组中的任何元素,所以期望为一半。最坏情况即为,给出的输入在序列的最后一个,或者无法找到。
2.2-4 应如何修改任何一个算法,才能使之具有良好的最好情况运行时间?
尽量避免出现遍历的循环,这样运行时间会出现n²及以上的量级。应该使用分组递归的方法。
2.3-1 使用图2-4作为模型,说明归并排序在数组A = {3,41,52,26,38,57,9,49}上的操作。
1.逐层分组
3,41,52,26 38,57,9,49
3,41 52,26 38,57 9,49
2.排序
3,41 26,52 38,57 9,49
3.归并
3,26,41,52 9,38,49,57
3,9,26,38,41,52,57
2.3-2 重写过程MERGE,使之不使用哨兵,而是一旦数组L或R的所有元素均被复制回A就立刻停止,然后把另一个数组的剩余部分复制回A。
因为我们知道从p到q一共多少个值,所以使用哨兵后我们就无需在取数之前判断数组是否已取完。如果不使用哨兵,则需要判断EOF。我们知道循环次数的信息无法用上,有点浪费。
//先将数组放置到两个新建的空数组内
//原数组为A[]数组中从p到r的一个数组,A【q】为A【p】 A【r】中的一个数
n1 = q - p + 1
n2 = r - q
let L[n1] and R[n2] be new array
for i = 1 to n1
{
L[i] = A[p + i - 1]
}
for i = 1 to n2
{
R[i] = A[q + i]
}
//分配好两个小数组
i = 1 //初始化i和j
j = 1
for k = p to r //新数组长度
{
if( L[i] < R[j])
{
A[k] = L[i]
i = i + 1
while(L[i] = EOF)
{
k = k + 1
A[k] = R[j]
j = j + 1
}
}
else
{
A[k] = R[j]
j = j + 1
while(R[j] = EOF)
{
k = k + 1
A[k] = L[i]
i = i + 1
}
}
}
//EOF代指判断数组末尾方法
2.3-3 使用数学归纳法证明:当n刚好是2的幂时,以下递归式的解是T(n) = nlgn。
T(n) = 2 若 n = 2
2T(n/2) + n 若 n = 2的k次方, k > 1
当k = 1的时候,可以分成两个长度为1的递归式,运行时间是nlgn
当k + 1的时候,相当于原来的两个式子递归为一个两倍长的式子,运行时间为(n+1)lg(n+1)
成立
2.3-4 我们可以把插入排序表示为如下的一个递归过程。为了排序A[1..n],我们递归地排序A[1..n - 1],然后把A[n]插入已排序的数组A[1..n - 1]。为插入排序的这个递归版本的最坏情况运行时间写一个递归式。
recursion(A,1,n)
{
if(n = 2)
{
if(A[2] > A[1])
return true;
else
{
tem = A[2]
A[2] = A[1]
A[1] = tem
return true;
}
}
if(n > 2)
{
recursion(A,1,n - 1)
j = n - 1
for(j > 0 and A[n] < A[j])
{
A[j + 1] = A[j] //向右移动一位
j = j - 1
}
A[j] = A[n]
return true;
}
}
2.3-5 回顾查找问题,注意到,如果序列A已排好序,就可以将该序列的中点与v进行比较。根据比较的结果,原序列中有一半就可以不用再作进一步的考虑了。二分查找算法重复这个过程,每次都将序列剩余部分的规模减半。为二分查找写出迭代或递归的伪代码。证明:二分查找的最坏情况运行时间为theta lgn
for(i = 1 to n)
{
min = 1
max = n
middle = (min + max)/2 //截断小数,不存在小数问题
if(key > A[middle])
{
min = A[middle]
if(max == min + 1)
return NIL
middle = (min + max)/2
}
else if(key < A[middle])
{
max = A[middle]
if(max == min + 1)
return NIL
middle = (min + max)/2
}
else
return A[middle]
}
1 to n
1 to n /2
1 to n /4
.
.
.
深度为lgn
2.3-6 注意到2.1节中的过程INSERTION-SORT的第5~7行的while循环采用一种线性查找来(反向)扫描已排好序的子数组A[1..j - 1]。我们可以使用二分查找来把插入排序的最坏情况改进到theta nlgn吗?
for i = 2 to n
{
key = A[i]
j = i - 1
//接下来进入二分查找
min = 1
max = i
middle = (min + max) / 2
if(A[middle] > key)
{
max = middle
if(max == min + 1)
{
for(i = n downto max)
A[n+1] = A[n]
A[max] = key
}
middle = (min + max) / 2
}
else if(A[middle] < key)
{
min = middle
if(max == min + 1)
{
for(i = n dowmto max)
A[n+1] = A[n]
A[max] = key
}
middle = (min + max) / 2
}
else
{
for(i = n dowmto midlle)
A[n+1] = A[n]
A[middle] = key
}
//放置数字的时候 将每个数字后移一位 复杂度仍然是n
//查找的复杂度从n变为lgn
//所以时间复杂度为 (lgn + n)*n = n²
}
2.3-7 描述一个运行时间为theta(nlgn)的算法,给定n个整数的集合S和另一个整数x,该算法能确定S中是否存在两个其和刚好为X的元素。
二分排序,时间复杂度为nlgn
排序好之后每个元素遍历一边 复杂度n,遍历时二分查找X - A[n]的值,复杂度为lgn
nlgn + nlgn = nlgn
Q2.1虽然归并排序的最坏情况运行时间为theta nlgn,而插入排序的最坏情况运行时间为theta n²,但是插入排序中的常量因子可能使得它在n较小时,在许多机器上实际运行的更快。因此,在归并排序中当子问题变得足够小时,采用插入排序来使递归的叶变粗是有意义的。考虑对归并排序的一种修改,其中使用插入排序来排序长度为k的n/k个子表,然后使用标准的合并机制来合并这些子表,这里k是一个待定的值。
a.证明:插入排序最坏情况可以在theta nk时间内排序每个长度为k的 n/k个子表
排序一个长度为k的子表所用时间为 k²,排序n/k个所用时间为theta nk
b.表明在最坏情况下如何在theta nlg(n/k)时间内合并这些子表
待合并的子表一共有n/k个,一共排序lg(n/k)次,每次排n个(每次排序n/k个,排n/2k次,每个2k个),所以theta = nlg(n/k)
k越大合并越快,但是排序慢
c.假定修改后的算法的最坏情况运行时间为theta (nk + nlg(n/k)),要使修改后的算法与标准的归并排序具有相同的运行时间,作为n的一个函数,借助theta记号,k的最大值是什么?
theta(nk + nlg(n/k)) = theta(nlgn)
k + lg(n/k) = lgn
k < lgn
//当k大于lgn后,修改后的算法复杂度上升。
d.在实践中,我们应该如何选择k?
综合排序和归并的算法效率,选取k + lg(n/k)取最小值时的k值,因为实际上这两项前还有别的系数,所以需要根据实际情况确定。
Q2-2 冒泡排序是一种流行但低效的排序算法,它的作用是反复交换相邻的未按次序排列的元素。
BUBBLESORT(A)
for i = 1 to A.length - 1
for j = A.length downto i + 1
if A[j] < A[j - 1]
exchange A[j] with A[j - 1]
a.假设A’表示BUBBLESORT(A)的输出。为了证明BUBBLESORT正确,我们必须证明它将终止并且有:
A’[1] <= A'[2] <= A’[3] … A’[n]
其中n = A.length。为了证明BUBBLRSORT确实完成了排序,我们还需要证明什么?
原先数组未排序 and 新数组中数据全部来自A
下面两部分将证明不等式
b.为第2~4行的for循环精确地说明一个循环不变式,并证明该循环不变式成立。你的证明应该使用本章中给出的循环不变 式证明的结构。
初始化:对于第一个排序的数字A’[1],它是数组中所有元素遍历一遍之后选出的最小项
保持:两两比较后,较大的向左移动,所以在剩下的元素中,每次抵达左端的都是“剩下数组”中最小的数
终止:当i = A.length - 1时,只剩两个元素需要比较,比较后排序即完成。