Chapter 2 Foundation of Algorithms

Chapter 2


插入排序
线性查找
选择算法
归并排序算法
二分查找算法
冒泡排序

插入排序


INSERTION-SORT(A)
for j=2 to A.length
    key = A[j]
    //insert A[j] into the sortes sequence A[1...j-1]
    i = j-1
    while i>0 and A[i]>key
        A[i+1] = A[i]
        i = i-1
    A[i+1] = key    
function insertion_sort(arr) {
  var key = 0;
  var i = 0;
  for(var j = 1; j < arr.length; j++) {
    key = arr[j];
    i = j - 1;
    while (i >= 0 && arr[i] > key) {
      arr[i+1] = arr[i];
      i = i - 1;
    }
    arr[i+1] = key;
  }
  return arr;
}

循环不变式


初始化:循环第一次迭代之前,它为真
保持:如果循环的某次迭代之前它为真,那么下次迭代之前它仍为真
终止:在循环终止时,不变式为我们提供一个有用的性质,该性质有助于证明算法是正确的

算法分析


INSERTION-SORT(A) 代价 次数
for j=2 to A.length c1 n
key = A[j] c2 n-1
//insert A[j] into the sortes sequence A[1...j-1] 0 n-1
i = j-1 c4 n-1
while i>0 and A[i]>key c5 ∑tj (2<=j<=n)
A[i+1] = A[i] c6 ∑(tj - 1) (2<=j<=n)
i = i-1 c7 ∑(tj - 1) (2<=j<=n)
A[i+1] = key c8 n-1

tj 表示对那个值j第5行执行while循环测试的次数
所以,T(n) = c1n + c2(n-1) + c4(n-1) + c5∑tj (2<=j<=n) + c6∑(tj - 1) (2<=j<=n) + c7∑(tj - 1) (2<=j<=n) + c8(n-1)
最佳情况:对j=2, 3, ..., n,有tj = 1,所以 T(n) = (c1 + c2 + c4 + c5 +c8)n - (c2 + c4 + c5 + c8) = Θ(n)
最坏情况:对j=2, 3, ..., n,有tj = j,所以 ∑tj (2<=j<=n) = ∑j (2<=j<=n) = n(n+1)/2 - 1, ∑(tj - 1) (2<=j<=n) = n(n-1)/2, T(n) = (c5 + c6 + c7)n^2/2 + (c1 + c2 + c4 + c5/2 - c6/2 - c7/2 + c8)n - (c2 + c4 + c5 + c8) = Θ(n^2)

分治模式


分治模式在每层递归时都有三个步骤:
分解原问题为若干个子问题,这些字问题是原问题的规模较小的实例。
解决这些子问题,递归地求解各子问题。然而,若子问题的规模足够小,则直接求解。
合并这些子问题的解成原问题的解。

归并排序算法


归并排序算法完全遵循分治模式。直观上其操作如下:
分解:分解待排序的n个元素的序列成各具n/2个元素的两个子序列。
解决:使用归并排序递归地排序两个子序列。
合并:合并两个已排序的子序列以产生已排序的答案。

通过调用一个辅助过程MERGE(A, p, q, r)来完成两个已排序序列的合并,其中A是一个数组,p、q和r是数组下标,满足 p<=q

MERGE(A, p, q, r)
n1 = q - p + 1
n2 = r - q
Let L[1..n1+1] and R[1..n2+1] be new arrays
for i = 1 to n1
    L[i] = A[p + i - 1]
for j = 1 to n2
    R[j] = A[q+j]
//插入哨兵牌
L[n1 + 1] = ∞
R[n2 + 1] = ∞
i = 1
j = 1
for k = p to r
    if L[i] <= R[j]
        A[k] = L[i]
        i = i + 1
    else A[k] = R[j]
        j = j + 1
MERGE-SORT(A, p, r)
if p < r
    q = └(p + r)/2┘
    MERGE-SORT(A, p, q)
    MERGE-SORT(A, q + 1, r)�
    MERGE(A, p, q, r)

分析分治算法


分治算法运行时间的递归式来自基本模式的单个步骤。假设T(n)是规模为n的一个问题的运行时间。若问题规模足够小,如对某个常量c,n<=c,则直接求解需要常量时间,将其记作Θ(1)。假设把原问题分解成a个子问题,每个子问题的规模是原问题的1/b。为了求解一个规模为n/b的子问题,需要T(n/b)的时间,所以,需要aT(n/b)的时间来求解a个子问题,如果分解问题成子问题需要时间D(n),合并子问题的解成原问题的解需要C(n),那么得到递归式:
若n<=c,T(n) = Θ(1);其他,T(n) = aT(n/b) + D(n) + C(n)

所以,归并排序的最坏情况运行时间T(n)的递归式:
若n=1,T(n) = Θ(1);若n>1,T(n) = 2T(n/2) + Θ(n)
等价于:若n=1,T(n) = c;若n>1,T(n) = 2T(n/2) + cn
对递归式T(n) = 2T(n/2) + cn构造递归树,如下:

结果递归树

所以,T(n) = Θ(nlgn)

练习


2.1-2 重写INSERTION-SORT,使之按非升序排列
INSERTION-SORT2(A)
for j=2 to A.length
    key = A[j]
    i = j-1
    while i > 0 and A[i] < key
        A[i+1] = A[i]
        i = i-1
    A[i+1] = key
function insertion_sort2(arr) {
  var key = 0;
  var i = 0;
  for(var j = 1; j < arr.length; j++) {
    key = arr[j];
    i = j - 1;
    while (i >= 0 && arr[i] < key) {
      arr[i+1] = arr[i];
      i = i - 1;
    }
    arr[i+1] = key;
  }
  return arr;
}
2.1-3 考虑一下查找问题:
输入:n个数的一个序列 A= 和一个值v
输出:下标i是的 v=A[i] 或者当v不在A中出现时,v为特殊值NIL
写出线性查找的伪代码,它扫描整个序列来查找v。使用一个循环不变式来证明你的算法是正确的。确保你的循环不变式满足三条必要的性质。
LINEAR-SEARCH(A, v)
for i=1 to A.length
    if A[i] equals v
        return i
return NIL
function linear_search(arr, v) {
  for(var i = 0; i < arr.length; i++) {
    if (arr[i] === v) {
      return i+1;
    }
  }
  return null;
}
2.1-4 考虑把两个n位二进制整数加起来的问题,这两个整数分别存储在两个n元数组A和B中。这两个整数的和应按二进制形式存储在一个(n+1)元数组C中。请给出该问题的形式化描述,并写出伪代码。

输入:两个n元数组A和B
输出:一个(n+1)元数组C

BINARY-ADD(A, B)
for i=A.length to 1
    tmp = A[i] + B[i] + tmp;
    if tmp > 1
        C[i+1] = tmp - 2
        tmp = 1
    else C[i+1] = tmp
        tmp = 0
C[1] = tmp          
function binary_add(arr1, arr2) {
  var arr3 = [];
  var tmp = 0;
  for(var i = arr1.length-1; i >= 0; i--) {
    tmp = arr1[i] + arr2[i] + tmp;
    if (tmp > 1) {
      arr3[i+1] = tmp - 2;
      tmp = 1;
    }
    else {
      arr3[i+1] = tmp;
      tmp = 0;
    }
  }
  arr3[0] = tmp;
  return arr3;
}
2.2-1 用Θ记号表示函数n3/1000-100n2-100n+3

Θ(n^3)

2.2-2 考虑排序存储在数组A中的n个数:首先找出A中的最小元素并将其与A[1]中的元素进行交换。接着,找出A中的次最小元素并将其与A[2]中的元素进行交换。对A中前n-1个元素按该方式继续。这算法称为选择算法,写出其伪代码。该算法维持的循环不变式是什么?为什么它只需要对前n-1个元素,而不是对所有n个元素运行?用Θ记号给出选择排序的最好情况与最坏情况运行时间
SELECTION(A)
for j=1 to A.length-1
    min = A[j]
    pointer = j
    for i=j to A.length
        if A[i] < min
            min = A[i]
            pointer = i
    A[pointer] = A[j]
    A[j] = min      
function selection(arr) {
  var min = 0;
  var pointer = 0;
  for(var j = 0; j < arr.length - 1; j++) {
    min = arr[j];
    pointer = j;
    for(var i = j; i 

因为第n个元素在进过前n-1个循环后一定是最大的,所以不需要对它在进行操作。
Θbest = Θ(n^2)
Θworest = Θ(n^3) Θ(n^2)

2.2-3 再次考虑线性查找问题(参见练习2.1-3)。假定要查找的元素等可能地为数组中的任意元素,平均需要检查的输入序列的多少元素?最坏情况又如何?用Θ记号给出线性查找的平均情况和最坏情况运行时间。证明你的答案

平均情况 T(n) = 1/(n+1) + 2/(n+1) + 3/(n+1) + ... + n/(n+1) + n/(n+1) = (n^2 + 3n)/2(n+1) = Θ(n)
最坏情况 T(n) = Θ(n)

2.3-2 重写过程MERGE,使之不使用哨兵,而是一旦数组L或R的所有元素均被复制回A就立刻停止,然后把两一个数组的剩余部分复制回A
MERGE2(A, p, q, r)
n1 = q - p + 1
n2 = r - q
let L[1..n1] and R[1.. n2] be new arrays
for i = 1 to n1
    L[i] = A[p + i - 1]
for j = 1 to n2
    R[j] = A[q + j]
i = 1
j = 1
for k = p to r
    if (L[i] <= R[j] or j > n2)
        A[k] = L[i]
        i = i + 1
    else if (L[i] > R[j] or i > n1)
        A[k] = R[j]
        j = j + 1
var arr = [10,7,3,9,2,9,4,5,6,11];
function merge2(p, q, r) {
    var n1 = q - p + 1;
    var n2 = r - q;
    var arrL = [];
    var arrR = [];
    var i = 0, j = 0;
    for (i = 0; i < n1; i++) {
        arrL[i] = arr[p + i];
    }
    for (j = 0; j < n2; j++) {
        arrR[j] = arr[q + j + 1];
    }
    i = 0;
    j = 0;
    var k = 0;
    for (k = p; k < r + 1; k++) {
        if (arrL[i] <= arrR[j] || j >= n2) {
            arr[k] = arrL[i];
            i = i + 1;
        } else if (arrL[i] > arrR[j] || i >= n1) {
            arr[k] = arrR[j];
            j = j + 1;
        }
    }
}
function merge_sort(p, r) {
    var mid = 0;
    if (p < r) {
        mid = Math.floor((p + r)/2);
        merge_sort(p, mid);
        merge_sort(mid+1, r);
        merge2(p, mid, r);
    }
}
merge_sort(0, arr.length-1);
console.log(arr);
2.3-4 我们可以把插入排序表示为如下的一个递归过程。为了排序A[1..n],我们递归地排序A[1..n-1],然后把A[n]插入到已排序的数组A[1..n-1]。为插入排序的这个递归版本的最坏情况运行时间写一个递归式

如果n = 1,T(1) = 1;
如果n > 1,将A[n]插入A[1..n-1]需要比进行n-1次比较,所以 T(n) = T(n-1) + c(n-1)

2.3-5 回顾线性查找问题,注意到,如果序列A已排好序,就可以将该序列的中点与v进行比较。根据比较的结果,原序列中有一半就可以不用再做进一步的考虑了。二分查找算法重复这个过程,每次都将序列剩余部分的规模减半。为二分查找写出迭代或递归的伪代码。证明:二分查找的最坏运行时间为Θ(lgn)
BINARY-SEARCH(A, key)
start = 1
end = A.length
while (start <= end)
    i = └(start + end) / 2┘
    if (A[i] == key)
        return i
    else if (A[i] > key)
        end = i - 1
    else if (A[i] < key)
        start = i + 1
return "NF"
function binary_search(array, key) {
    var start = 0, end = array.length-1;
    var i = 0;
    while (start <= end) {
        i = Math.floor((start + end)/2);
        if (array[i] === key) {
            return i;
        } else if (array[i] > key) {
            end = i - 1;
        } else if (array[i] < key) {
            start = i + 1;
        }
    }
    return "NF";
}

如果n = 1,T(1) = 1;
如果n > 1, 最差的情况就是比较到最后一位,得出结论,所以T(n)=lgn

2.3-6 注意插入排序的第5~7行的while循环采用一种线性查找来(反向)扫描已排好序的子数组A[1..j-1]。我们可以使用二分查找来把插入排序的最坏情况运行总时间改进到Θ(nlgn)吗?
INSERTION-SORT2(A)
for i=2 to A.length
    newA = A.slice(0,1)
    key = A[i]
    answer = BINARY-SEARCH(newA, key)
    for j=i down to answer
        A[j] = A[j-1]
    A[answer] = key
function binary_search(array, key) {
    var start = 0, end = array.length-1;
    var i = 0;
    while (start <= end) {
        i = Math.floor((start + end)/2);
        if (array[i] === key) {
            return i;
        } else if (array[i] > key) {
            end = i - 1;
        } else if (array[i] < key) {
            start = i + 1;
        }
    }
    return (start < end)? end: start;
}
function insertion_sort2(array) {
    for (var i = 1; i < array.length; i++) {
        var new_array = array.slice(0, i);
        var key = array[i];
        var answer = binary_search(new_array, key);
        for(var j = i; j > answer; j--) {
            array[j] = array[j-1];
        }
        array[answer] = key;
    }
    return array;
}
2.3-7 描述一个运行时间为Θ(nlgn)的算法,给定n个整数的集合S和另一个整数x,该算法能确定S中是否存在两个其和刚好为x的元素。
function determineSumX(array, x) {
    var filtered = array.filter(function(e) {
        return e x) {
            end = end - 1;
        }
    }
    return false;
}
function binary_search(array, key) {
    var start = 0, end = array.length-1;
    var i = 0;
    while (start <= end) {
        i = Math.floor((start + end)/2);
        if (array[i] === key) {
            return i;
        } else if (array[i] > key) {
            end = i - 1;
        } else if (array[i] < key) {
            start = i + 1;
        }
    }
    return (start < end)? end: start;
}
function insertion_sort2(array) {
    for (var i = 1; i < array.length; i++) {
        var new_array = array.slice(0, i);
        var key = array[i];
        var answer = binary_search(new_array, key);
        for(var j = i; j > answer; j--) {
            array[j] = array[j-1];
        }
        array[answer] = key;
    }
    return array;
}
2.1 (在归并排序中对小数组采用插入排序)虽然归并排序的最坏情况运行时间为Θ(nlgn),而插入排序的最坏运行时间为Θ(n^2),但是插入牌组中的常量因子可能使得它在n较小时,在许多机器上实际运行得更快。因此,在归并排序中,当子问题变得足够小时,采用插入排序来使递归的叶变粗是有意义的。考虑对归并排序的一种修改,其中使用插入排序来排序长度为k的n/k个子表,然后使用标准的合并机制来合并这些子表,这里k是一个待定的值。
a. 证明:插入排序最坏的情况可以在Θ(nk)时间内排序每个长度为k的n/k个子表。
b. 表明在最坏的情况下如何在Θ(nlg(n/k))时间内合并这些子表。
c. 假定修改后的算法的最坏情况运行时间为Θ(nk+nlg(n/k)),要使修改后的算法与标准的归并排序具有相同的运行时间,作为n的一个函数,借助Θ记号,k的最大值是什么?
d. 在实践中,我们应该如何选择k?

a. 因为插入排序的最坏运行时间为Θ(n2),所以对于长度为k的1个子表,最坏运行时间为Θ(k2);
又因为一共有n/k个子表,所以,最坏运行时间为(n/k)*Θ(k^2) = Θ(nk)。
b. 由结果递归树可得,最小的叶子节点为cn/k,共有k个叶子节点,所以树的高度为lg(n/k),又每层将贡献总代价cn,所以,总代价为cn(lg(n/k)+1),也就是Θ(nlog(n/k))。
c. Θ(nk + nlg(n/k)) = Θ(nlgn),=>Θ(k+lg(n/k)) = Θ(lgn),所以k 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(A)
1 for i = 1 to A.length - 1
2   for j = A.length downto i + 1
3       if A[j] < A[j-1]
4           exchange A[j] with A[j-1]
a. 假设A′表示BUBBLESORT(A)的输出。为了证明BUBBLESORT正确,我们必须证明它将终止并且有:
        A'[1] ≤ A'[2] ≤ ... ≤ A'[n]                           (2.3)
其中 n=A.length。为了证明BUBBLESORT确实完成了排序我们还需要证明什么?
下面两个部分将证明不等式(2.3)。
b. 为第2~4行的for循环精确地说明一个循环不变式,并证明该循环不变式成立。你的证明应该使用本章中给出的循环不变式证明的结构。
c. 利用在(b)部分证明的循环不变式的终止条件,为第1~4行的for循环说明一个循环不变式,它可以用来证明不等式(2.3)。你的证明应采用本章中给出的循环不变式的证明结构。
d. 冒泡排序算法的最坏情况运行时间是多少?与插入排序的运行时间相比,其性能如何?

a. 当n=1时,能够正确排序。
b. 对第2~4行的for循环,循环不变式是A[j]是子数组A[j…n]中的最小值,且子数组中的元素并未改变。约定:n=A.length。
初始化:开始时,j=n,子数组中只包含A[n],故循环不变式成立
保持:假设对于任意的一个j,使得A[j]是子数组A[j…n]中的最小值,在下一轮循环中,若A[j] < A[j-1],则A[j]和A[j-1]交换。使得A[j-1]是子数组A[j-1…n]中的最小值,循环不变式依然成立
终止:循环结束时j=i,A[j]是子数组A[j…n]中的最小值,且子数组中的元素并未改变。
c. 对于1~4行的for循环,循环不变式是每次循环前,A[1…i-1]中包含了整个数组中前i-1小的排好序的元素,而A[i…n]中包含剩下的元素。
初始化:第一次循环前i=1,子数组为空,循环不变式成立
保持:假设对于任意一个i,使得A[1…i-1]中包含了整个数组中前i-1小的排好序的元素,而A[i…n]中包含剩下的元素,则内层循环保证了A[i]是子数组A[i…n]中的最小元素,则A[1…i]中包含了整个数组中前i小的排好序的元素,而A[i+1…n]中包含剩下的元素。循环不变式成立
终止:循环结束时i=n+1,则A[1…n]中包含了整个数组中前n小的排好序的元素,即数组有序。
d. 冒泡排序最坏和最好运行时间均为Θ(n^2)
插入排序的最坏运行时间为Θ(n^2),但是最好运行时间为Θ(n)
排序前A所有元素已经有序时,插入排序达到最好运行时间。

2.3 (霍纳(Horner)规则的正确性)给定系数a0, a1, …, an和x的值,代码片段
1 y = 0
2 for i = n downto 0
3   y = ai + x·y
实现了用于求值多项式
的霍纳规则。
a. 借助Θ记号,实现霍纳规则的以上代码片段的运行时间是多少?
b. 编写伪代码来实现朴素的多项式求值算法,该算法从头开始计算多项式的每个项。该算法的运行时间是多少?与霍纳规则相比,其性能如何?
c. 考虑以下循环不变式:
在第2~3行for循环每次迭代的开始有
把没有项的和式解释为等于0。遵照本章中给出的循环不变式证明的结构,使用该循环不变式来证明终止时有
d. 最后证明上面给出的代码片段将正确地求由系数a0, a1, …, an刻画的多项式的值。

a. Θ(n)
b. 伪代码如下:

y = 0
tmp = 1
for k = 0 to n
    y = y + ak * tmp
    tmp = tmp * x 

该算法的时间复杂度为:Θ(n^2)
c. 证明如下:
初始化:i=n时,y=0
保持:对于任意 0 ≤ i ≤ n,迭代的开始有y=Σ a[k+i+1] * x^k =a[k+i+1] + a[k+i+2] * x + ... + an * x^(n-(i+1)),循环后有:y[i] = a[i] + y[i+1] * x = a[i] + (a[i+1] * x + a[i+2] * x + ... + a[n] * x^(n-(i+1))) * x = a[i] + a[i+1] * x + a[i+2] * x^2 + ... + a[n] * x^(n-i)
终止:i=0时,循环终止,i=0开始前有y=Σ a[k+1] * x^k = a[1] + a[2] * x + a[n] * x^(n-1),执行循环,y=a0 + x * [a[1] + a[2] * x + a[n] * x^(n-1)] = a0 + a[1]x + a[2]x^2 + ... + a[n]*x^n。
d. <我并没有看懂>

Chapter 2 Foundation of Algorithms_第1张图片
Answer2-3d.png

2.4 (逆序对)假设A[1..n]是一个有n个不同数的数组。若i < j且A[i] > A[j],则对偶(i, j)称为A的一个逆序对(inversion)。
a. 列出数组 <2, 3, 8, 6, 1> 的5个逆序对。
b. 由集合 {1, 2,…,n} 中的元素构成的什么数组具有最多的逆序对?它有多少逆序对?
c. 插入排序的运行时间与输入数组中逆序对的数量之间是什么关系?证明你的回答。
d. 给出一个确定在n个元素的任何排列中逆序对数量的算法,最坏情况需要 Θ(nlgn) 时间。(提示:修改归并排序。)

a. (2, 1), (3, 1), (8, 6), (8, 1), (6, 1)
b. 拥有最多的逆序对,共有 n*(n-1)/2
c. 逆序对越多,插入排序的复杂度越大
排序n个数,如果不存在逆序对,则插入排序的复杂度为Θ(n),每多一个,复杂度加1
d. 如下:

MERGE(A, p, q, r)
n1 = q - p + 1
n2 = r - q
Let L[1..n1+1] and R[1..n2+1] be new arrays
for i = 1 to n1
    L[i] = A[p + i - 1]
for j = 1 to n2
    R[j] = A[q+j]
//插入哨兵牌
L[n1 + 1] = ∞
R[n2 + 1] = ∞
i = 1
j = 1
count = 0
for k = p to r
    if L[i] <= R[j]
        A[k] = L[i]
        i = i + 1
    else A[k] = R[j]
        j = j + 1
        count = count + 1

你可能感兴趣的:(Chapter 2 Foundation of Algorithms)