Python數據結構與算法--算法分析


在計算機科學中,算法分析Analysis of algorithm)是分析執行一個給定算法需要消耗的計算資源數量(例如計算時間,存儲器使用等)的過程。算法的效率或復雜度在理論上表示為一個函數。其定義域是輸入數據的長度,值域通常是執行步驟數量(時間復雜度)或者存儲器位置數量(空間復雜度)。算法分析是計算復雜度理論的重要組成部分。

本文地址:http://www.cnblogs.com/archimedes/p/python-datastruct-algorithm-analysis.html,轉載請注明源地址。

一個有趣的問題經常出現,那就是兩個看似不同的程序,到底哪個更好呢?

要回答這個問題, 我們必須知道程序和代表程序的算法有很大的區別. 算法是一個通用的, 解決問題的一條條的指令. 提供一個解決任何具有指定輸入的實例問題方法, 算法產生期望的結果. 一個程序, 另一方面, 是將算法用某一門編程語言代碼實現. 有很多的程序實現的同一算法, 取決於程序員和編程語言的使用.

進一步的探究這種差異, 考察下面的函數代碼. 這個函數解決一個簡單的問題, 計算前n個自然數的和. 解決方案遍歷這 n 個整數, 相加后賦值到累加器.

def sumOfN(n):
   theSum = 0
   for i in range(1,n+1):
       theSum = theSum + i
   return theSum

print(sumOfN(10))

接下來看下面的代碼. 第一眼看上去感覺很奇怪, 但是深入理解之后你將發現這個函數和上面的函數完成同樣的工作. T原因是這個函數不是那么明顯,代碼難看. 我們沒有使用好的變量名導致可讀性很差, 並且還聲明了沒有必要聲明的變量.

def foo(tom):
    fred = 0
    for bill in range(1,tom+1):
       barney = bill
       fred = fred + barney
    return fred
print(foo(10))

到底哪段代碼更好呢.問題的答案取決於你的標准.如果你只關注可讀性,函數sumOfN 肯定比 foo 好. 事實上, 你可能在你的編程啟蒙課上見到過很多教你編寫可讀性好和易於理解的程序的例子. 然而在這里, 我們還對算法感興趣. 

作為替代空間的需求, 我們基於它們執行時間來分析和比較算法. 這種度量有時候被稱為算法的“執行時間”或"運行時間". 我們測量 sumOfN 函數執行時間的一種方法是做個基准分析. 在Python, 我們可以通過一個函數針對我們所使用的系統上標記程序的起始和結束時刻. 在 time 模塊有一個被稱為 time 的函數,將返回系統的當前時間. 通過兩次調用這個函數, 起始和結束, 然后計算差值, 我們可以得到准確的執行時間.

Listing 1

import time

def sumOfN2(n):
   start = time.time()

   theSum = 0
   for i in range(1,n+1):
      theSum = theSum + i

   end = time.time()

   return theSum,end-start

Listing 1 展示了sumOfN 函數在求和前后的時間開銷. 測試結果如下:

>>>for i in range(5):
       print("Sum is %d required %10.7f seconds"%sumOfN(10000))
Sum is 50005000 required  0.0018950 seconds Sum is 50005000 required  0.0018620 seconds Sum is 50005000 required  0.0019171 seconds Sum is 50005000 required  0.0019162 seconds Sum is 50005000 required  0.0019360 seconds

我們發現時間相當的一致並且都平均花費 0.0019 秒執行程序. 那么假如我們將n增大到 100,000 會怎樣呢?

>>>for i in range(5):
       print("Sum is %d required %10.7f seconds"%sumOfN(100000))
Sum is 5000050000 required  0.0199420 seconds Sum is 5000050000 required  0.0180972 seconds Sum is 5000050000 required  0.0194821 seconds Sum is 5000050000 required  0.0178988 seconds Sum is 5000050000 required  0.0188949 seconds >>>

再次, 時間更長, 非常的一致, 平均10倍的時間. 將 n 增大到 1,000,000 我們達到:

>>>for i in range(5):
       print("Sum is %d required %10.7f seconds"%sumOfN(1000000))
Sum is 500000500000 required  0.1948988 seconds Sum is 500000500000 required  0.1850290 seconds Sum is 500000500000 required  0.1809771 seconds Sum is 500000500000 required  0.1729250 seconds Sum is 500000500000 required  0.1646299 seconds >>>

在這種情況下, 平均執行時間又一次被證實是之前的10倍.

現在來看一下 Listing 2, 提出了一個不同的解決求和問題的方法. 這個函數, sumOfN3, 運用了一個等式:∑ni = (n+1)n/2來計算 n 個自然數取代循環計算.

Listing 2

def sumOfN3(n):
   return (n*(n+1))/2

print(sumOfN3(10))

如果我們針對 sumOfN3 做一些測試, 使用5種不同的n值(10,000, 100,000, 1,000,000, 10,000,000, and 100,000,000), 我們得到下面的結果:

Sum is 50005000 required 0.00000095 seconds Sum is 5000050000 required 0.00000191 seconds Sum is 500000500000 required 0.00000095 seconds Sum is 50000005000000 required 0.00000095 seconds Sum is 5000000050000000 required 0.00000119 seconds

對於這個輸出,有兩個方面需要注意. 第一, 上面程序的運行時間比前面的任意一個的運行時間都短. 第二, 無論n為多大執行時間都是一致的. 

但是這個標准真正地告訴我們什么?直觀地說, 我們可以看到,迭代的解決方案似乎是因為一些程序步驟被重復而做更多的工作. 這是它占用更多運行時間可能的原因. 當我們增加 n的時候循環方案執行時間也在增加. 然而,有一個問題. 如果我們跑相同的功能在不同的計算機或使用不同的編程語言,我們可能會得到不同的結果. 如果是老式計算機將可能在 sumOfN3上執行更多的時間.

我們需要一種更好的方式來描述這些算法的執行時間。基准的方法計算實際的執行時間。它並不真的為我們提供了一個有用的測量,因為它是依賴於特定的機器,當前時間,編譯,和編程語言。相反,我們要有一個特性,是獨立於程序或計算機的使用。這一方法將獨立地判斷使用的算法是有用的,可以用來在實現算法比較。


一個易位構詞實例

一個展示算法不同的數量級的例子是經典的字符串易位問題. 一個字符串和另一個字符串如果僅僅是字母的位置發生改變我們就稱為易位. 例如, 'heart' 和 'earth' 就互為易位. 字符串'python'和 'typhon' 也是. 為簡化問題的討論,我們假設字符串中的字符為26個英文字母並且兩個字符串的長度相同. 我們的目標是寫一個boolean 類型的函數來判斷兩個給定的字符串是否互為易位.

方法1: 逐一檢測

對於易位問題,我們的第一個解決方案是檢測第一個字符串的每一個字母是否在第二個字符串中. 如果成功檢測所有的字母, 那么兩個字符串是易位的. 檢查一個字母成功后將使用 Python的特殊值 None 取代. 然而, 因為在 Python 中string是不可變的, 第一步將字符串轉換成 list. 看下面的代碼:

def anagramSolution1(s1,s2):
    alist = list(s2)
    pos1 = 0
    stillOK = True

    while pos1 < len(s1) and stillOK:
        pos2 = 0
        found = False
        while pos2 < len(alist) and not found:
            if s1[pos1] == alist[pos2]:
                found = True
            else:
                pos2 = pos2 + 1

        if found:
            alist[pos2] = None
        else:
            stillOK = False

        pos1 = pos1 + 1

    return stillOK

print(anagramSolution1('abcd','dcba'))

方法2: 排序比較

另一個解決方案基於的思想是:即使兩個字符串 s1 和 s2 不同, t它們易位當且僅當它們包含完全相同的字母集合. 因此, 如果我們首先將兩個字符串的字符按照字典排序, 如果兩個字符串易位,那么我們將得到完全一樣的兩個字符串. 在 Python 我們可以使用list的內建方法 sort 來簡單的實現排序.看下面的代碼:

def anagramSolution2(s1,s2):
    alist1 = list(s1)
    alist2 = list(s2)

    alist1.sort()
    alist2.sort()

    pos = 0
    matches = True

    while pos < len(s1) and matches:
        if alist1[pos]==alist2[pos]:
            pos = pos + 1
        else:
            matches = False

    return matches

print(anagramSolution2('abcde','edcba'))

第一眼看上去,你可能認為程序的時間復雜度為O(n), 因為只有一個簡單的比較n個字母的循環. 然而, 兩次調用 Python sort 函數都沒有考慮開銷. 以后我們會介紹, 排序將花費的時間復雜度為 O(n2) 或 O(nlogn), 於是排序相比循環占主導地位.

方法3: 暴力

一個 brute force 計數方法是枚舉出所有的可能性. 對於這個問題, 我們可以使用 s1 的字母簡單地生成所有的可能字符串並看 s2 是否出現. 然而,這種方法有一個難點. 我們列舉出s1的所有可能性,第一個字母有 n 種可能,第二個位置有n-1種可能, 第三個位置有n-2種可能,……. 總共的可能性為:n*(n-1)*(n-1)*3*2*1 = n!.已經證明 n!遞增非常快,當n非常大的時候, n! 遞增速度超過 2n .

方法4: 計算和比較

最后一個解決方案是基於這樣的一個事實:任意兩個易位的字符串都有相同的'a'的數目,相同的'b'的數目,相同的'c'的數目……. 為了判斷兩個字符串是否易位,我們首先計算每一個字母的次數. 因為只有26個可能的字母, 我們可以使用一個list來保存26個計數, 每一個保存可能的字母. 每次當我們看到一個特別的字母,我們就增加對應的計數. 最后, 如果兩個list的對應計數完全相同, 兩個字符串就是易位的. 看下面的代碼:

def anagramSolution4(s1,s2):
    c1 = [0]*26
    c2 = [0]*26

    for i in range(len(s1)):
        pos = ord(s1[i])-ord('a')
        c1[pos] = c1[pos] + 1

    for i in range(len(s2)):
        pos = ord(s2[i])-ord('a')
        c2[pos] = c2[pos] + 1

    j = 0
    stillOK = True
    while j<26 and stillOK:
        if c1[j]==c2[j]:
            j = j + 1
        else:
            stillOK = False

    return stillOK

print(anagramSolution4('apple','pleap'))

依然, 這種解決方案包含大量的循環. 然而, 與第一種方案不同, 它們都沒有被嵌入. 前兩個循環方案都在n的基礎上計算字母. 第三個方案的循環, 比較兩個字符串中counts的數目, 只需要 26 步 因為一個字符串只有26種可能的字母. 累加在一起我們得到 T(n)=2n+26 步. 即是 O(n). 我們找到了這個問題的線性時間解法.

離開這個例子之前,我們需要說的是空間開銷.雖然最后的解決方案能夠在線性時間內運行,它能成功必須要通過使用額外的存儲保持兩個列表中的字符數。換句話說,該算法使用了空間換時間.

這是一種常見的情況. 在許多場合,你需要做出決定的時間和空間之間的權衡。在目前的情況下,額外空間量是不顯著的。然而,如果下面的字母有數百萬字,就必須更多的關注空間開銷。作為一個計算機科學家,當在選定算法的時候,主要由你來決定如何利用計算機資源來解決一個特定的問題.

您還可能感興趣:

Python數據結構與算法--面向對象

Python數據結構與算法--數據類型

Python基礎(10)--數字 

Python基礎(9)--正則表達式

Python基礎(8)--文件

Python基礎(7)--函數

Python基礎(6)--條件、循環 

Python基礎(5)--字典

Python基礎(4)--字符串

Python基礎(3)--列表和元組

Python基礎(2)--對象類型 

Python基礎(1)--Python編程習慣與特點

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM