[学习《算法导论》] 第一部分 基础知识

为什么学习这本书

这学期选修了 《算法设计》这门课,冲着能补下短板。科大的本科生是会修使用《算法导论》做教材学习《算法》课的。对于算法,我的积累只停留在考研《数据结构》的水平上。深知自己的欠缺。所以打算利用这学期的时间把《算法导论》给啃掉,力求深入理解其中的算法,做好课后习题,并且先用C++来实现其中的算法。(之后可能会用C#再来一遍)哈哈,废话不多说,开始看吧。希望每周能打三到四次卡。

第 1 章 算法在计算中的作用

文中提到的一些名词:
- 旅行商问题。Traveling purchaser problem

文中的一句话:
-是否具有算法知识与技术的坚实基础是区分真正熟练的程序员与初学者的一个特征。

练习

1.2-3 n 的最小值为何值时,运行时间为 100n2 的一个算法在相同机器上快于运行时间为 2n 的另一个算法?

这里使用一个小的脚步来执行这两个值得比较,能够很快得出结论:

/*
* 算法导论 练习 1.2-3
*/
#include 
#include   
#include 

using namespace std;

int main(void)
{
    for(int i = 1; i < 30; ++i)
    {
        cout << left;
        cout << setw(6) << 100 * i * i << " - " << setw(6) << pow(2, i) << " = " << setw(6) << 100 * i * i - pow(2, i);
        if(100 * i * i - pow(2, i) < 0)
        {
            cout << " **************** i = " << i << endl;
            break;
        }
        cout << endl;
    }

    return 0;
}

/*
输出:

100    - 2      = 98
400    - 4      = 396
900    - 8      = 892
1600   - 16     = 1584
2500   - 32     = 2468
3600   - 64     = 3536
4900   - 128    = 4772
6400   - 256    = 6144
8100   - 512    = 7588
10000  - 1024   = 8976
12100  - 2048   = 10052
14400  - 4096   = 10304
16900  - 8192   = 8708
19600  - 16384  = 3216
22500  - 32768  = -10268 **************** i = 15
请按任意键继续. . .

*/

第 2 章 算法基础

  1. 考察插入排序算法,证明该算法能正确地排序并分析其运行时间。
  2. 引入用于算法设计的分治法。
  3. 使用分治法开发一个称为归并排序的算法,并分析归并排序的运行时间。

2.1 插入排序

插入排序的伪码

INSERTION - SORT(A)

for j = 2 to A.length
    key = A[j]
    // Insert A[j] into the sorted 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

循环不变式

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

练习

2.1-2 重写过程 INSERTION-SORT,使之按非升序(而不是非降序)排序。

答:只需要改变一下比较符号就可以了。

INSERTION - SORT(A)

for j = 2 to A.length
    key = A[j]
    // Insert A[j] into the sorted 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

2.1-3 考虑一下查找问题:

输入:n个数的一个序列 A=[a1,a2,…,an]和一个值v。
输出:下标i,使得v=A[i],或者当v不在A中时,输出NIL。
写出这个问题的线性查找的伪码,它顺序的扫描整个序列以找到v。利用循环不变式证明其正确性。

答:伪码如下

LINEAR - SEARCH

for i = 1 to A.length
    if A[i] == v
        return i
return NIL

证明:
循环不变式:每次迭代开始之前,A[1..i-1]都不包含v。
初始:i=1,A[1..0]=空,因此不包含v。
保持:在某一轮迭代开始之前,A[1..i-1]不包含v,进入循环体后,有两种情况:
(1)A[i]==v ,则直接return i,因此保持循环不变式。
(2)A[i]!=v,则进入下一轮循环,因此在下一轮迭代开始前保持循环不变式。
终止:i=n+1,A[1…n]不包含v,因此说明A不包含v,返回NIL。

2.1-4 考虑把两个n位二进制整数加起来的问题,这两个整数分别存储在两个n元数组A和B中。这两个整数的和应该按二进制形式存储在一个(n+1)元数组C中。请给出该问题的形式化描述,并写出伪代码。

答:(注:这里假定A[1]为最低位,A[n]为最高位。)
形式化描述:
循环不变式:循环的每次迭代开始前,C[1..i]保存着A[1..i-1]与B[1..i-1]的和。
初始:i=1,C[1]=0,人为给两个规定:A[1..0]和B[1..0]不包含任何元素;两个0位二进制数相加得0。在这两个规定下,显然C[1]保存着A[1..0]与B[1..0]的和。不变式成立。
保持:在循环的某次迭代之前,假设 i = k,C[1..k]保存着A[1..k-1]与B[1..k-1]的和。则,执行此次迭代,结果就是C[1..k+1]保存着A[1..k]与B[1..k]的和。下次迭代之前,i = k+1,由上次迭代的执行结果知C[1..k+1]保存着A[1..k]和B[1..k]相加的和,即C[1..i]保存着A[1..i-1]和B[1..i-1]相加的和。不变式成立。
终止:循环终止时,i = n+1,将不变式中的i替换为n+1,即C[1..n+1]保存着A[1..n]与B[1..n]的和。而A[1..n]和B[1..n]就是完整的两个二进制数,所以不变式成立。

伪代码:

ADD - BINARY
for i = 1 to n
    C[i+1] = (A[i] + B[i] + C[i]) / 2  // 向上进位
    C[i] = (A[i] + B[i] + C[i]) % 2    // 当前位

实验结果(C++)

2.2 分析算法

练习

2.2-2 写出选择排序的伪代码。该算法维持的循环不变式是什么?

伪代码:

SELECTION - SORT(A)
for i = 1 to A.length - 1
    min = i
    for j = i + 1 to A.length
        if A[min] > A[j]
            min = j
    if min != i
        swap A[min] A[i]

维持的循环不变式:在第一层for循环的每次迭代开始时(循环变量为i),A[1..i-1]子数组中元素为A[1..n]中最小的i-1个数字,且按从小到大排序。

实验代码

2.3 设计算法

2.3.1 分治法

思想:将原问题分解为几个规模较小但类似于原问题的子问题,递归的求解这些子问题,然后在合并这些子问题的解来建立原问题的解。

归并排序算法完全遵循分治模式。
分解:分解代培徐的n个元素的序列成各具n/2个元素的两个子序列。
解决:使用归并排序递归的排序两个子序列。
合并:合并两个已排序的子序列以产生已排序的答案。

归并排序算法的关键是“合并”步骤中将两个已经排序序列合并为一个。

下面的伪代码将 A[p..q]和A[q+1..r]这两个已经排序的子数组合并为一个。

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] = inf     // 每个堆得底部放置一张哨兵牌,包涵一个特殊值,在比较的时候不可能为较小的那个。
R[n2 + 1] = inf
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过程来设计MERGE-SORT算法。

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, r)

第 4 章 分治策略

4.1 最大子数组问题

数组A的和最大的非空连续子数组称为A的最大子数组。

使用分治策略的求解方法

假定我们要寻找子数组A[low..high]的最大子数组。使用分治法意味着我们要将子数组划分成两个规模尽量相等的子数组。也就是说,找到子数组的中央位置,比如mid,然后考虑求解两个子数组A[low..mid]和A[mid+1..high]。A[low..high]的任何连续子数组A[i..j]所处的位置必然是下列三种情况之一。

  • 完全位于子数组A[low..mid]中
  • 完全位于子数组A[mid+1..high]中
  • 跨越了中点

我们可以递归的求解前两种情况的最大子数组,因为这两个子问题仍是最大子数组问题,只是规模更小。因此剩下的工作就是寻找跨越中点的最大子数组,然后在三种情况中选取和最大者。

我们可以在线性时间内求出跨越中点的最大子数组。伪代码如下:

FIND_MAX_CROSSING_SUBARRAY(A, low, mid, high)
left_sum = -inf
sum = 0
for i = mid downto low
    sum += A[i]
    if sum > left_sum
        left_sum = sum
        max_left = i
right_sum = -inf
sum = 0
for j = mid + 1 to high
    sum += A[j]
    if sum > right_sum
        right_sum = sum
        max_right = j
return(max_left, max_right, left_sum + right_sum)

有了线性时间的FIND_MAX_CROSSING_SUBARRAY在手,我们就可以很清晰的设计求解最大子数组问题的分治算法的伪代码了:

FIND_MAXIMUM_SUBARRAY(A, low, high)
if low == high
    return(low, high, A[low])
else
    mid = (low + high) / 2
    (left_low, left_high, left_sum) = FIND_MAXIMUM_SUBARRAY(A, low, mid)
    (right_low, right_high, right_sum) = FIND_MAXIMUM_SUBARRAY(A, mid + 1, high)
    (cross_low, cross_high, cross_sum) = FIND_MAX_CROSSING_SUBARRAY(A, low, mid, high)
    if left_sum >= right_sum and left_sum >= cross_sum
        return(left_low, left_high, left_sum)
    elseif right_sum >= left_sum and right_sum >= cross_sum
        return(right_low, right_high, right_sum)
    else
        return(cross_low, cross_high, cross_sum)

练习

4.1-2 对最大子数组问题,编写暴力求解方法的伪代码,其运行时间应该为 Θ(n2)

解答:伪代码如下

FIND-MAXIMUM-SUBARRAY(A, low, high)
maxSum = -inf, leftPos = 0, rightPos = 0
for i = low to high
    currentSum = 0
    for j = i to high
        currentSum += A[j]
        if currentSum > maxSum
            maxSum = currentSum
            leftPos = i
            rightPos = j
return (leftPos, rightPos, maxSum)

4.1-5 为最大子数组问题设计一个非递归的、线性时间的算法。

解答:伪代码如下:

FIND-MAXIMUM-SUBARRAY-LINEAR(A, low, high)
maxSum = -inf
currentSum = 0
j = low
for i = low to high
    currentSum += A[i]
    if currentSum > maxSum
        maxSum = currentSum
        leftPos = j
        rightPos = i
    if currentSum < 0
        currentSum = 0
        j = i + 1
return(leftPos, rightPos, maxSum)

这三种求最大子数组算法的实现

第5章 概率分析和随机算法


5.3 随机算法

In common practice, randomized algorithms are approximated using a pseudorandom number generator in place of a true source of random bits; such an implementation may deviate from the expected theoretical behavior.

对于诸如雇佣问题之类的问题,其中,假设输入的所有排列等可能的出现往往有益,通过概率分析可以指导设计一个随机算法。我们不是假设输入的一个分布,而是设定一个分布。特别的,在算法运行前,先随机地排列应聘者,以加强所有排列都是等可能出现的性质。

随机排列数组

这里,我们讨论两种随机化方法。

1.PERMUTE-BY-SORTING(A)
为数组的每个元素A[i]赋一个随机的优先级P[i],然后依据优先级对数组A中的元素进行排序。耗时最大的为排序步骤,需要花费 Ω(nlg2n) 时间。

PERMUTE_BY_SORTING(A)
n = A.length
let P[1..n] be a new array
for i = 1 to n
    P[i] = RANDOM(1, n * n * n)
sort A, using P as sort keys

2.RANDOMIZE-IN-PLACE(A)
原址排列给定数组,在进行第 i 次迭代时,元素A[i]是从元素A[i]到A[n]中随机选取的。能够在O(n)时间内完成。

RANDOMIZE_IN_PLACE(A)
n = A.length
for i = 1 to n
    swap A[i] with A[RANDOM(i, n)]

关于这两种随机化方法确实能产生一个均匀随机排列的证明,见CLRS。

关于这两种随机化方法的实现——点我

练习

5.3-7 创建集合{1,2,3,…,n}的一个随机样本(有m个元素)。

RANDOM-SAMPLE(m,n)
if m == 0
    returnelse
    S = RANDOM-SAMPLE(m-1, n-1)
    i = RANDOM(1,n)
    if i ∈ S
        S = S ∪ {n}
    else
        S = S ∪ {i}
    return S

证明???

你可能感兴趣的:(数据结构与算法)