算法导论从零单排

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时,只剩两个元素需要比较,比较后排序即完成。

你可能感兴趣的:(算法导论从零单排)