算法筆記 -【複雜度分析<續>】

前文連接:算法筆記 -【複雜度分析】算法

從一個小栗子開始

function find(ar, x) {
    let i = 0
    let pos = -1
    for (; i < ar.length; i++) {
        if (ar[i] === x) {
            pos = i
        }
    }
    return pos
}

很容易看出上段代碼是爲了從數組ar中找出與x相等的項的索引,(細心的同窗可能會發現該方法返回的索引是最後一個匹配項,可是這裏咱們只討論算法複雜度問題,因此假設ar必定是一個沒有重複項的數組,這樣便於排除干擾)可是稍微分析就會發現上述代碼是優化空間的,由於假設在第1次遍歷的時候就找到了x的索引,後面的遍歷其實是沒有意義的。
優化:segmentfault

function find(ar, x) {
    let i = 0
    let pos = -1
    for (; i < ar.length; i++) {
        if (ar[i] === x) {
            pos = i
            break
        }
    }
    return pos
}

通過優化後的代碼理論上耗時應該會比以前的代碼更少,可是咱們怎麼衡量此次優化呢?經過以前的複雜度分析咱們知道先後兩段代碼的複雜度都爲O(n),但此時咱們就會有疑問了:優化後的代碼複雜度仍是O(n)數組

更細緻的複雜度分析

最好/最壞狀況時間複雜度

如前栗子,數據結構

function find(ar, x) {
    let i = 0
    let pos = -1
    for (; i < ar.length; i++) {
        if (ar[i] === x) {
            pos = i
            break
        }
    }
    return pos
}

代碼的理論執行耗時有些狀況不只與數據規模n有關,還跟具體的條件有關;
最好狀況時間複雜度O(1)(即在第一次遍歷就找到x)
最壞狀況時間複雜度O(n)(即在最後一次遍歷才找到x)性能

平均狀況時間複雜度

不管最好狀況時間複雜度仍是最壞狀況時間複雜度都是極端狀況,實際參考意義不大,這裏就須要引入一個新的概念:平均狀況時間複雜度優化

具體分析

要查找數組中x的索引其實有n+1中狀況:找不到和在位置0、一、2...n-1,每一中狀況的耗時除以n+1,即爲平均耗時:
(1 + 2 + 3 + ... n + n) / (n + 1)
-> n(n + 3) / 2(n + 1)
-> T(n) = O(n)
因此平均狀況時間複雜度爲O(n)code

分析方法改進

咱們前面的算法其實是創建在每種狀況出現的機率相同的基礎上,但實際上,每種狀況的出現機率並不相同:
從數組ar中查找x,首先x在ar中和不在ar中的機率都爲1/2,當x在ar中時,在每一個位置的機率都爲1/n,因此:
(1 * 1/n + 2 * 1/n + ... n * 1/n + n) / 2
-> (3n + 1) / 4(這個值就是加權平均值或者指望值
-> T(n) = O(n)(得出的時間複雜度爲加權平均時間複雜度指望時間複雜度
其實沒必要在乎概念,只要知道改進後的複雜度算法更加可靠和科學一點就好了
回到最初咱們進行的一個小優化:在找到x以後中止後面的遍歷,但通過一輪分析彷佛優化先後的複雜度沒有變化,那咱們這個優化還有意義嗎?答案是確定的,由於沒有優化前最好狀況複雜度和最壞狀況複雜度都爲O(n),複雜度是穩定的,代碼n次遍歷都會被執行,可是優化後,最好狀況複雜度爲O(1),最壞狀況複雜度爲O(n),複雜度是不穩定的,代碼的n次遍歷不在被必然執行索引

均攤時間複雜度

平均時間複雜度就能解決複雜度分析的全部問題嗎?
來看一個栗子:(僞代碼)get

let ar = new Array(n)
let amount = 0
function insert(val) {
    if (amount === ar.length) {
        let sum = 0
        for (let i = 0; i < ar.length; i++) {
            sum += ar[i]
        }
        ar = new Array(n)
        ar[0] = sum
        amount = 1
    }
    ar[amount] = val
    amount++
}

這裏咱們嘗試用以前複雜度分析的方法對其進行分析:
第一次執行,即ar爲長度爲n的空數組,計數amount爲0,此時不須要進行遍歷,複雜度爲O(1)
第二次執行,若是n > 2,則同第一次執行,複雜度爲O(1)
...
第n次執行,同第一次執行,複雜度爲O(1)
第n+1次執行,須要將數組ar中全部項求和放入ar[0],而後再進行插入,此時須要進行數組求和,複雜度爲O(n),此後數組ar剩餘長度爲n-1
咱們發現此後n-1次執行的複雜度都爲O(1),而後就會再次出現一次複雜度爲O(n)的執行,也就是說該段程序的複雜度呈現必定的規律:每出現一次O(n)的複雜度,餘下的n-1次的複雜度就爲O(1),這樣周而復始
最好時間複雜度:O(1)
最壞時間複雜度:O(n)
指望平均時間複雜度:1 * 1 /(n + 1) + 1 * 1 /(n + 1) + ... n * (n + 1) -> O(1)數學

可是在求指望平均時間複雜度的時候和前一小結的栗子有點不一樣:最好和最壞時間複雜度出現是有規律的(每次出現一個O(n)複雜度的操做後面必跟着n-1個O(1)複雜度的操做),並非徹底隨機的,若是單純用平均時間複雜度描述其耗時趨勢不是很準確,因此這裏就須要一種新的描述方式:均攤時間複雜度
每一次 O(n) 的插入操做,都會跟着 n-1 次 O(1) 的插入操做,因此把耗時多的那次操做均攤到接下來的 n-1 次耗時少的操做上,均攤下來,這一組連續的操做的均攤時間複雜度就是 O(1)
結論:
對一個數據結構進行一組連續操做中,大部分狀況下時間複雜度都很低,只有個別狀況下時間複雜度比較高,並且這些操做之間存在先後連貫的時序關係
通常均攤時間複雜度就等於最好狀況時間複雜度。

總結

前文總結下來的內容是關於時間複雜度的分析多引入了幾個概念:

  • 最好時間複雜度
  • 最壞時間複雜度
  • 指望平均時間複雜度
  • 均攤時間複雜度

這些複雜度描述方法只是做爲複雜度分析的一個補充,在實際中更多地做爲代碼優化的一個參考,所謂概念可有可無,重要的是這裏面涉及到的一些簡單的數學分析方法,瞭解了這些方法在實際應用中就能夠更好得組織咱們的代碼,同時預知一部分性能問題。

相關文章
相關標籤/搜索