1. 程式人生 > >《資料結構與演算法分析》學習筆記-第二章-演算法分析

《資料結構與演算法分析》學習筆記-第二章-演算法分析

演算法分析

如果解決一個問題的演算法被確定下來,並用某種證明方法證明其是正確的,那麼接下來就要判斷該演算法的執行時間,以及執行時佔用的空間。這一章主要討論

  • 估算程式執行時間
  • 降低程式的執行時間
  • 遞迴的風險
  • 將一個數自乘得到其冪以及計算兩個數的最大公因數的有效演算法

2.1 數學基礎

  1. 如果存在正常數c和n0使得當N >= n0時,T(N) <= cf(N),則記為T(N) = 0(f(N)).這裡說的是T(N)的增長趨勢不超過f(N)的增長趨勢。我們常說的時間複雜度就用的這裡的定義,f(N)也稱為T(N)的上界
  2. 如果存在正常數c和n0使得當N >= n0時,T(N) >= cg(N),則記為T(N) = Ω(f(N)).這裡說的是T(N)的增長趨勢不小於g(N)的增長趨勢。這裡是說g(N)是T(N)的下界
  3. T(N) = Θ(h(N)),當且僅當T(N) = O(h(N)),且T(N) = Ω(h(N))。這裡是說T(N)和g(N)的增長趨勢是一樣的
  4. 如果T(N) = O(p(N)),且T(N) != Θ(p(N)),則T(N) = o(p(N))。這裡是說T(N)的增長趨勢總是小於p(N)的。而且沒有相等的情況

上述說法實在太過晦澀了。舉一個簡單的例子。當g(N) = N^2時,g(N) = O(N^3),g(N) = O(N^4)都是對的。g(N) = Ω(N), g(N) = Ω(1)也都是對的。g(N) = Θ(N^2)則表示g(N) = O(N^2),g(N) = Ω(N^2)。即當前的結果時最符合g(N)本身的增長趨勢的。如圖所示:

有三條重要的法則需要記住:

  1. 如果T1(N) = O(f(N)),且T2(N) = O(g(N)),那麼
    • T1(N) + T2(N) = max(O(f(N)), O(g(N))),
    • T1(N) * T2(N) = 0(f(N) * g(N))
  2. 如果T(N)是一個k次多項式,則T(N) = Θ(N^k)
  3. 對任意常數k,log^k N = O(N)。它告訴我們對數增長的非常緩慢

在用大O表示法的時候,要保留高階次冪,丟棄常數項和低階次冪。通過增長率對函式進行分類如圖:

我們總能通過計算極限lim f(N) / g(N) (n->∞)來確定兩個函式f(N)和g(N)的相對增長率。可以使用洛必達準則進行計算。

  • 極限是0,則f(N) = o(g(N))
  • 極限是c 且c != 0,則f(N) = Θ(g(N))
  • 極限是∞,則g(N) = o(f(N))
  • 極限擺動:兩者無關

比如,f(N) = NlogN和g(N) = N^1.5的相對增長率,即可計算為f(N) / g(N) = logN / N^0.5 = log^2 N / N。又因為N的增長要快於logN的任意次冪。所以g(N)的增長快於f(N)的增長

洛必達準則:若lim f(N) = ∞ (n->∞)且lim g(N) = ∞ (n->∞).則lim f(N)/g(N) = lim f'(N)/g'(N) (n->∞)。

2.2 模型

為了便於分析問題,我們假設一個模型計算機。它執行任何一個基礎指令都消耗一個時間單元,並且假設它有無限的記憶體。

2.3 要分析的問題

  1. 如果是很小輸入量的情形,則花費大量的時間去設計聰明的演算法就不值得
  2. 資料的讀入是一個瓶頸,一旦資料讀入,好的演算法問題就會迅速解決。因此要使演算法足夠有效而不至於成為問題的瓶頸是很重要的

2.4 執行時間計算

2.4.1 例子

  • 如果兩個演算法所花費的時間大致相同,那麼判斷哪個程式更快的最好方法是將它們編碼並執行
  • 為簡化分析,我們採用大O表示法計算執行時間,大O是一個上界。所以分析結果是為了給程式在最壞情況下能夠在規定時間內執行完成提供保障。程式可能提前結束,但不會延後
// 書上例程
// 計算i^3的累加求和
int sum (int N)
{
    int i, PartialSum;
    PartialSum = 0;             /*1*/
    for(i = 1; i <= N; i++)     /*2*/
        PartialSum += i * i * i;/*3*/
    return PartialSum;          /*4*/
}

這裡針對每行進行分析:

  1. 花費1個時間單元:1個賦值
  2. 花費1+N+1+N=2N+2個時間單元:1個賦值、N+1次判斷、N次加法
  3. 花費N(2+1+1)=4N個時間單元:2個乘法、1個加法、1個賦值,執行N次
  4. 花費1個時間單元:1個返回

合計花費1+2N+2+4N+1=6N+4個時間單元。

但是實際上我們不用每次都這樣分析,因為面對成百上千行的程式時,我們不可能每一行都這樣分析。只需計算最高階。能夠看出for迴圈佔用時間最多。因此時間複雜度為O(N)

2.4.2 一般法則

  1. for迴圈:一次for迴圈執行時間至多應是該for迴圈內語句的執行時間乘以迭代次數
  2. 巢狀的for迴圈:從裡向外分析迴圈。在一組巢狀迴圈內部的一條語句總的執行時間為該語句執行時間乘以該組所有for迴圈的大小的乘積
for (i = 0; i < N; i++)
    for (j=0; j < N; j++)
        k++;    // 1 * N * N = N^2,時間複雜度為O(N^2)
  1. 順序語句:將各個語句執行時間求和即可。取最大值。
for (i = 0; i < N; i++)
    A[i] = 0;   // O(N)
for (i = 0; i < N; i++)
    for (j = 0; j < N; j++)
        A[i] += A[j] + i + j;   // O(N^2)
// 總時間為O(N) + O(N^2),因此取最高階,總時間複雜度為O(N^2)
  1. if-else語句:判斷時間加上兩個分支中較長的執行時間

我們要避免在遞迴呼叫中做重複的工作。

2.4.3 最大子序列和問題的解

最大子序列問題:給定整數A1, A2, ... , AN(可能有負數),求任意連續整數和的最大值。如果所有整數均為負數,則最大子序列和為0

  1. 方案一,時間複雜度O(N^3)
// 書上例程
int
MaxSubsequenceSum(const int A[], int N)
{
    int ThisSum, MaxSum, i, j, k;
    
    MaxSum = 0;
    for (i = 0; i < N; i++) {
        for (j = i; j < N; j++) {
            ThisSum = 0;
            for (k = i; k <= j; k++) {
                ThisSum += A[k];
            }
            
            if (ThisSum > MaxSum) {
                MaxSum = ThisSum;
            }
        }
    }
    
    return MaxSum;
}
  1. 方案二,時間複雜度O(N^2)。和方案一相比丟棄了最內層的迴圈
int
MaxSubsequenceSum(const int A[], int N)
{
    int ThisSum, MaxSum, i, j, k;
    
    MaxSum = 0;
    for (i = 0; i < N; i++) {
        ThisSum = 0;
        for (j = i; j < N; j++) {
            ThisSum += A[k];
            if (ThisSum > MaxSum) {
                MaxSum = ThisSum;
            }
        }
    }
    
    return MaxSum;
}
  1. 方案三,時間複雜度O(NlogN)。使用分治策略。‘分’為將資料分為左右兩部分,即將問題分成兩個大致相等的子問題,然後遞迴的將他們求解;‘治’為分別算出兩部分的最大子序列和,再將結果合併。這個問題中,最大子序列和可能出現三種情況:左半部分,右半部分,跨越左半部分和右半部分(包含左半部分的最後一個元素和右半部分的第一個元素)。第三種情況的最大子序列和為包含左半部分最後一個元素的最大子序列和加上包含右半部分第一個元素的最大子序列和的總和。
// 書上例程
int 
max3(int a, int b, int c)
{
    int x;
    x = a > b? a: b;
    return (x > c? x: c);    
}

int
MaxSubsequenceSum(const int A[], int Left, int Right)
{
    int MaxLeftSum, MaxRightSum;
    int MaxLeftBorderSum, MaxRightBorderSum;
    int MaxLeftThisSum, MaxRightThisSum;
    int Center;
    int cnt;
    
    if (Left == Right) {
        if (A[Left] > 0) {
            return A[Left];
        } else {
            return 0;
        }
    }
    
    Center = (Left + Right) / 2;
    MaxLeftSum = MaxSubsequenceSum(A, Left, Center);
    MaxRightSum = MaxSubsequenceSum(A, Center + 1, Right);
    
    MaxLeftBorderSum = 0;
    MaxLeftThisSum = 0;
    for (cnt = Center; cnt >= Left; cnt--) {
        MaxLeftThisSum += A[cnt];
        if (MaxLeftThisSum > MaxLeftBorderSum) {
            MaxLeftBorderSum = MaxLeftThisSum;
        }
    }
    
    MaxRightBorderSum = 0;
    MaxRightThisSum = 0;
    for (cnt = Center + 1; cnt <= Right; cnt++) {
        MaxRightThisSum += A[cnt];
        if (MaxRightThisSum > MaxRightBorderSum) {
            MaxRightBorderSum = MaxRightThisSum;
        }
    }
    
    return max3(MaxLeftSum, MaxRightSum, MaxRightBorderSum + MaxLeftBorderSum);
}
  1. 方案四,時間複雜度為O(N)。只對資料進行一次掃描,一旦讀入並被處理,它就不需要被記憶。如果陣列儲存在磁碟上,它就可以被順序讀入,在主存中不必儲存陣列的任何部分。而且任意時刻,演算法能對它已經讀入的資料給出子序列問題的正確答案。具有這種特性的演算法也叫做聯機演算法(線上演算法)。僅需要常量空間並以線性時間執行的線上演算法幾乎是完美的演算法
//書上例程
int
MaxSubsequenceSum(const int A[], int N)
{
    int ThisSum, MaxSum, j;
    
    ThisSum = MaxSum = 0;
    for (j = 0; j < N; j++) {
        ThisSum += A[j];
        if (ThisSum > MaxSum) {
            MaxSum = ThisSum;
        } else if (ThisSum < 0) {
            ThisSum = 0;
        }
    }
    return MaxSum;
}

2.4.4 執行時間中的對數

如果一個演算法用常數時間(O(1))將問題的大小消減為其一部分(通常是1/2),那麼該演算法就是O(logN)。另一方面,如果使用常數時間只是把問題減少一個常數(如將問題減少1),那麼這種演算法就是O(N)的

  1. 對分查詢:對分查詢提供了時間複雜度為O(logN)的查詢操作。它的前提是資料已經排好序了,而且每當要插入一個元素,其插入操作的時間複雜度為O(N)。因為對分查詢適合元素比較固定的情況。
// 書上例程,時間複雜度為O(logN)
#define NotFound -1

int BinarySearch(const ElementType A[], ElementType X, int N)
{
    int low, high, mid;
    low = 0;
    high = N - 1;
    mid = (low + high) / 2;
    
    while (low <= high) {
        if (A[mid] < X) {
            low = mid + 1;
        } else if (A[mid] > X) {
            high = mid - 1;
        } else {
            return mid;
        }
    }
    return NotFound;
}
  1. 歐幾里得演算法:歐幾里得演算法這個名字聽起來很高大上,其實就是我們所說的輾轉相除法。當求兩個整數的最大公因數時,使用其中一個整數去除另一個整數得到餘數。再用剛才的除數去除以餘數得到新餘數,以此類推,當新餘數為0時,當前整式中的除數就為最大公因數。在兩次迭代之後,餘數最多是原始值的一半。迭代次數最多是2logN=0(logN)
// 書上例程:輾轉相除法,時間複雜度O(logN)
int test(unsigned int M, ungisned int N)
{
    unsigned int Rem;
    
    while (N > 0) {
        Rem = M % N;
        M = N;
        N = Rem;
    }
    return M;
}
  • 定理2.1:如果M > N,則 M mod N < M / 2。
    證明:如果N <= M / 2,則餘數必然小於N,所以M mod N < M / 2; 如果N > M / 2,則M - N < M / 2,即M mod N < M / 2。定理得證
  1. 冪運算:求一個整數的冪。即X^N。所需要的乘法次數最多是2logN,因此把問題分半最多需要兩次乘法(N為奇數的情況)
// 書上例程,時間複雜度O(logN)
long int Pow(long int X, unsigned int N)
{
    if (N == 0) {
        return 1;
    } else if (N == 1) {
        return X;
    }
    
    if (isEven(N)) {
        return Pow(X * X, N / 2);
    } else {
        return Pow(X * X, N / 2) * X;
    }
}

2.4.5 檢驗你的分析

  1. 方法一:實際程式設計,觀察執行時間結果與分析預測出的執行時間是否匹配。當N擴大一倍時,線性程式的執行時間乘以因子2,二次程式的執行時間乘以因子4,三次程式的執行時間乘以因子8.以對數時間執行的程式,當N增加一倍時,其執行時間只增加一個常數。以O(NlogN)執行的程式則是原來執行時間的兩倍多一點時間。(NX,2N(X+1)).如果低階項的係數相對較大,而N又不是足夠的大,那麼執行時間很難觀察清楚。單純憑實踐區分O(N)和O(NlogN)是很困難的
  2. 方法二:對N的某個範圍(通常是2的倍數隔開)計算比值T(N)/f(N),其中T(N)是觀察到的執行時間,f(N)則是理論推匯出的執行時間。如果所算出的值收斂於一個正常數,則代表f(N)是執行時間的理想近似;如果收斂於0,則代表f(N)估計過大;如果結果發散(越來越大),則代表f(N)估計過小。
//書上例程,時間複雜度O(N^2)
void test(int N)
{
    int Rel = 0, Tot = 0;
    int i, j;
    
    for( i = 1; i <= N; i++) {
        for ( j = i + 1, j <= N; j++) {
            Tot++;
            
            if (Gcd(i,j) == 1) {
                Rel++;
            }
        }
    }
    
    printf("%f", (double)Rel / Tot);
}

2.4.6 分析結果的準確性

有時分析會估計過大。那麼或者需要分析的更細緻,或者平均執行時間顯著小於最壞情形的執行時間而又沒辦法對所得的界加以改進。許多演算法,最壞的界實通過某個不良輸入達到的,但是實踐中它通常是估計過大的。對於大多數這種問題,平均情形的分析是極其複雜的,或者未解決的。最壞情形的界有些過分悲觀但是它是最好的已知解析結果。

  • 簡單的程式未必能有簡單的分析
  • 下界分析不止適用於某個演算法而是某一類演算法
  • Gcd演算法和求冪演算法大量應用在密碼學中

敬告:

本文原創,歡迎大家學習轉載_

轉載請在顯著位置註明:

博主ID:CrazyCatJack

原始博文連結地址:https://www.cnblogs.com/CrazyCatJack/p/12688582.html


第二章到此結束,接下來就到第三章了,開始具體的資料結構和演算法的實現講解了,滿滿乾貨哦!覺得好的話請可以點個關注 & 推薦,方便後面一起學習。謝謝大家的支援!

CrazyCatJack<