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

爲什麼學習這本書

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

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

文中提到的一些名詞:
- 旅行商問題。Traveling purchaser problem

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

練習

1.2-3 n 的最小值爲何值時,運行時間爲 100n2 的一個算法在相同機器上快於運行時間爲 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    // 當前位

實驗結果(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

證明???

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章