前文連接:算法筆記 -【複雜度分析】算法
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)
結論:
對一個數據結構進行一組連續操做中,大部分狀況下時間複雜度都很低,只有個別狀況下時間複雜度比較高,並且這些操做之間存在先後連貫的時序關係
通常均攤時間複雜度就等於最好狀況時間複雜度。
前文總結下來的內容是關於時間複雜度的分析多引入了幾個概念:
這些複雜度描述方法只是做爲複雜度分析的一個補充,在實際中更多地做爲代碼優化的一個參考,所謂概念可有可無,重要的是這裏面涉及到的一些簡單的數學分析方法,瞭解了這些方法在實際應用中就能夠更好得組織咱們的代碼,同時預知一部分性能問題。