1. 程式人生 > >玩轉資料結構——均攤複雜度和防止複雜度的震盪(筆記)

玩轉資料結構——均攤複雜度和防止複雜度的震盪(筆記)

資料規模

這裡寫圖片描述

時間複雜度

並不是所有的雙層迴圈都是O(n^2)的

這裡寫圖片描述

複雜度實驗來確定複雜度

這裡寫圖片描述

// O(N) 兩倍增加
int findMax( int arr[], int n ){

     assert( n > 0 );

     int res = arr[0];
     for( int i = 1 ; i < n ; i ++ )
         if( arr[i] > res )
             res = arr[i];

     return res;
 }

這裡寫圖片描述

隨後,O(n^2),資料規模乘二,時間複雜度乘4……


隨著資料的增加,可以看到O(logN)
這裡寫圖片描述

遞迴演算法時間複雜度分析

不是有遞迴的函式就一定是O(nlogn)
深入:主定理
這裡寫圖片描述

resize的複雜度分析——均攤複雜度 amortized time complexity

均攤分析和平均情況時間複雜度,前者是一個序列的操作取平均值,後者是針對不同輸入來計算平均值
動態陣列(Vector)每一個操作增加一個元素,刪除一個元素相應的複雜度,就需要Amortized Time
動態棧,動態佇列類似(陣列)

對於新增操作來說,最壞的情況是addLast(e)的時候,也需要進行resize,那麼複雜度就是O(n)級別的了。但是我們忽略了個問題:我們根本不可能每次操作的時候都會觸發resize,因此我們使用最壞的情況分析新增操作的時間複雜度是不合理的
這裡寫圖片描述


17次基本操作包含了9次新增操作 + 8次元素轉移操作
平均,每次addLast操作,進行2次基本操作( 17/9 約等於2 )
假設capacity=n,n+1次addLast,觸發resize,總共進行2n+1次基本操作
平均,每次addLast操作,進行2次基本操作( 2n+1/n+1 約等於 2 )
將1次resize的時間平攤給了n+1次addLast的時間,於是得到了平均每次addLast操作進行2次基本操作的結論
這樣均攤計算,時間複雜度是O(1)級別的,這和我們陣列中有多少個元素是沒有關係的
在這個例子裡,這樣均攤計算,比計算最壞情況是有意義的,這是因為最壞的情況是不會每次都出現的。
關於均攤複雜度,其實在很多演算法書中都不會進行介紹,但是在實際工程中,這樣的一個思想是蠻有意義的:就是一個相對比較耗時的操作,如果我們能保證他不會每次都被觸發的話,那麼這個相對比較耗時的操作它相應的時間是可以分攤到其它的操作中來的。
同理,我們看removeLast操作,均攤複雜度也為O(1)

resize的複雜度分析——複雜度震盪
但是,當我們同時看addLast和removeLast操作的時候:
假設我們現在有一個數組,這個陣列的容量為n,並且現在也裝滿了元素,那麼現在我們再呼叫一下addLast操作,顯然在新增一個新的元素的時候會需要擴容(擴容會耗費O(N)的時間),之後我們馬上進行removeLast操作(根據我們之前的邏輯,在上一個操作裡通過擴容,容量變為了2n,在我們刪除1個元素之後,元素又變為了n = 2n/2,根據我們程式碼中的邏輯,會觸發縮容的操作,同樣耗費了O(n)的時間);那麼我們如果再addLast、removeLast…等相繼依次操作
這裡寫圖片描述
對於addLast和removeLast來說,都是每隔n次操作都會觸發resize,而不會每次都觸發
但是現在我們製造了一種情景:同時看addLast和removeLast的時候,每一次都會耗費O(n)的複雜度,那麼這就是複雜度的震盪
resize的複雜度分析——出現複雜度震盪的原因及解決方案
removeLast時resize過於著急(採用了Eager的策略: 一旦我們的元素變為當前容積的1/2的時候,我們馬上就把當前的容積也縮容為1/2)
解決方案: Lazy (線上段樹中,也會用到類似的思路)
當元素變為當前容積的1/2時,不著急把當前容積縮容,而是等等;如果後面一直有刪除操作的話,當刪除元素到整個陣列容積的1/4時,那麼這樣看來我們的陣列確實用不了這麼大的容積,此時我們再來進行縮容,縮容整個陣列的1/2(這樣,即便我們要新增元素,也不需要馬上觸發擴容操作)
當 size == capacity / 4時,才將capacity減半