《數據結構與算法分析》學習筆記-第二章-算法分析

算法分析

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

  • 估算程序運行時間
  • 下降程序的運行時間
  • 遞歸的風險
  • 將一個數自乘獲得其冪以及計算兩個數的最大公因數的有效算法

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

原始博文連接地址:http://www.javashuo.com/article/p-tggaygmo-hu.html


第二章到此結束,接下來就到第三章了,開始具體的數據結構和算法的實現講解了,滿滿乾貨哦!以爲好的話請能夠點個關注 & 推薦,方便後面一塊兒學習。謝謝你們的支持!

CrazyCatJack
相關文章
相關標籤/搜索