1. 程式人生 > >容器擴容之分攤時間復雜度分析

容器擴容之分攤時間復雜度分析

之前 超過 ret 分析 因此 style expect ++ 算法設計

以向量vector為例分析動態擴容算法設計和時間復雜度分析

擴容算法實現

如何實現擴容,新的容量取多少合適?

對於容器內部數據區為數組的容器來說,動態擴容是必須的,因為無法預測容器規模的增長,而且必須保證數據區不僅在邏輯上連續分布存儲,循秩訪問,更要保證其在物理地址上的連續,因此每次插入操作前都需要詢問是否需要擴容?

技術分享圖片

如圖2.1(c~e)我們需要申請一個更大容量的連續物理地址作為新的數據區域如數組B【】,然後將原數組的數據復制到新數據區域中(圖d),此時才可以插入新元素e,最後,原數據區域的空間地址一定要釋放掉歸還給操作系統。

一種可行的算法實現如下

  1. void Vector<T>::expand() //
    向量空間不足時擴容
  2. {
  3. if(_size<_capacity) //尚未滿員時,不必擴容
  4. return ;
  5. if(_capacity<DEFAULT_CAPACITY) //不低於最小容量
  6. _capacity=DEFAULT_CAPACITY;
  7. T * _oldelem=_elem;
  8. _elem=new T[_capacity<<1]; //容量加倍
  9. for(int i=0;i<_size;i++) //復制原向量內容(T為基本類型,或者已經重載過"="運算符的自定義類型)
  10. _elem[i]=_oldelem[i];
  11. delete []oldelem; //釋放原空間
  12. }

由上述算法實現可以知道,新數組容量擴容至原數組容量的2倍!!

分攤分析

時間代價

可擴充向量和常規數組相比,其更加靈活,容量不受初始容量的限制,但是需要付出代價。插入操作的時間,在最壞情況下,每次擴容都是由n~2n,需要花費O(n)時間,看起來插入效率好像被拉低了,但是這是錯覺。 按照約定,每花費O(n)時間實施一次擴容,數組容量都會加倍,這意味著至少再需要經過n次插入操作,才會因為可能移除而在此擴容。即隨著向量規模不斷擴大,在執行插入操作過程之前需要進行擴容的概率將迅速降低,在某種平衡意義而言,用於擴容的時間成本不至於很高-----下面就是分攤時間復雜度的分析

分攤復雜度

對可擴充向量足夠多次連續操作,並將期間所消耗的時間,分攤至所有的操作。如此分攤平均至每次操作的時間成本,稱為分攤運行時間(amortized running time)。註意與平均時間復雜度(average running time)的區別:後者按照某種假定的概率分布,對各種情況下所需執行時間進行加權平均,也成為期望運行時間(expected running time)。而前者要求,參與分攤的操作必須構成和來自於一個真實可行的操作序列,而且該序列必須足夠長。 相對而言,分攤復雜度可以針對計算成本和效率做出更為客觀而準確的估計。

O(1)分攤時間

考察連續n次(查詢,插入,刪除等)操作,將所有的操作中用於數組擴容的時間累加起來,除以n,只要n足夠大,這一事件就是用於擴容處理的分攤時間成本。

假定數組初始容量為某一常數N,既然是估計復雜度上界,古設向量初始規模為N----即將溢出。不難知道,除插入操作之外,其他操作都不會導致溢出,考察最壞情況下,假設此之後連續n此操作都是insert,n>>N,定義如下函數

  • 1 size(n)=連續插入n個元素之後的向量規模
  • 2 capacity(n)=連續插入n個元素之後向量容量
  • 3 T(n)=為連續插入n個元素而花費的擴容時間

向量規模從N開始隨著操作的繼承逐步遞增:size(n)=n+N

既然不至於溢出,load factor 裝填因子不超過100%,算法擴容采用惰性策略,只有在的確發生溢出狀況下才容量加倍,即裝填因子始終不低於50%

即可得如下關系式

Size(n)<=capacity(n)<2*size(n)

考慮N為常數 有capacity(n)=O(size(n))=O(n)

容量以2位比例按指數速度增長,在容量達到capacity(n)之前,共做過O(logn)次擴容,每次擴容所需時間現行正比於當時容量,且同樣以2為比例按照指數增長。因此,消耗在擴容的時間累計:

T(n)=2N+4N+8N+…+capacity(n)<2capacity(n)=O(n)

將其分攤到其間的連續操作n次,單次操作所需的分攤運行時間應該為O(1)

技術分享圖片

技術分享圖片

容器擴容之分攤時間復雜度分析