1. 程式人生 > >[學習《演算法導論》] 第一部分 基礎知識

[學習《演算法導論》] 第一部分 基礎知識

為什麼學習這本書

這學期選修了 《演算法設計》這門課,衝著能補下短板。科大的本科生是會修使用《演算法導論》做教材學習《演算法》課的。對於演算法,我的積累只停留在考研《資料結構》的水平上。深知自己的欠缺。所以打算利用這學期的時間把《演算法導論》給啃掉,力求深入理解其中的演算法,做好課後習題,並且先用C++來實現其中的演算法。(之後可能會用C#再來一遍)哈哈,廢話不多說,開始看吧。希望每週能打三到四次卡。

第 1 章 演算法在計算中的作用

文中的一句話:
-是否具有演算法知識與技術的堅實基礎是區分真正熟練的程式設計師與初學者的一個特徵。

練習

1.2-3 n的最小值為何值時,執行時間為 100

n2 的一個演算法在相同機器上快於執行時間為 2n 的另一個演算法?

這裡使用一個小的腳步來執行這兩個值得比較,能夠很快得出結論:

/*
* 演算法導論 練習 1.2-3
*/
#include <iostream>
#include <iomanip>  
#include <cmath>

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    // 當前位

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

證明???