數據結構與算法分析 - 3 - 向量
C++STL中的vector模板類非常好用,有效解決了數組大小固定的問題。
而vector本身是封裝好的,一般使用時只需要知道vector提供的接口即可,而它的內部是怎樣實現的一直沒有去了解。
看了鄧公的數據結構,收獲頗多。
1.秩:一個元素的秩就是它的前驅元的個數(它的前面的元素的個數),各元素的秩互異。
通過秩(記為r)可以唯一確定向量中的一個元素,這是向量獨有的元素訪問方式,稱為循秩訪問。
2.向量中的元素:向量中的元素不必為基本類型,也不必是可以比較大小的數值型。
可以是更具一般性的某一類的對象。(在C++中可以理解為,向量的元素可以是一個結構體或者類)
3.向量支持的ADT接口
圖片來源:《數據結構(C++語言版)第三版》 鄧俊輝
了解過C++vector就知道,上面的大部分接口,C++提供的模板類都是支持的。
4.向量內部結構的本質
描述:向量結構在內部維護一個元素類型為T的私有數組,其容量由私有變量capacity指示,其有效元素數量(即當前向量的實際規模)由變量size指示。
//註意區別capacity和size,capacity可以理解為一個水桶的容量,而size相當於裝在水桶中的水的體積,所以恒有size<=capacity
意思是,在向量這個結構內部,系統創建了一個數組,這個數組的元素不必是基本類型(C++template實現),描述其大小的數值儲存在私有變量capacity中。
數組的大小是固定的,而一直以來vector都是作為變長數組來使用的,所以向量結構的核心在於,能對數組進行擴容。
擴容的方法很簡單,在向向量中添加元素時,將當前的size+1,與capacity進行比較,如果size+1<=capacity,那麽該元素成功添加到向量中(水桶不滿),
如果size+1>capacity,也就是即將發生數組越界時(水桶滿了),向量將new一個容量更大的數組,將原來數組中的元素復制到新的更大的數組中去,然後銷毀原數組,釋放空間。
通俗一點講就是,小水桶裝滿了,就買一個更大的水桶,把小水桶中的水倒進去,然後把小水桶扔了防止占空間。
這就是向量實現數組擴容的基本思路,然而,怎樣擴容,擴容多大合適,是有講究的。
5.構造與析構
默認構造方法:向量被系統創建,然後借助構造函數初始化。默認構造方法為,根據創建者指定的容量向系統申請儲存空間,創建數組_elem[]。若容量沒有指定,則使用容量默認值DEFAULT_CAPACITY。_size初始化為0。
重載構造函數:
基於復制的構造方法:
1 //基於數組A的區間[lo,hi]構造 2 template <typename T> 3 void Vector<T>::copyFrom(T const*A, Rank lo, Rank hi) 4 { 5 //分配空間 6 _elem = new T[_capacity = 2 * (hi - lo)]; 7 _size = 0; 8 //逐一復制 9 while (lo < hi) 10 _elem[_size++] = A[lo++]; 11 }
析構方法:析構函數不能重載
1 ~Vector(){delete []_elem;}
忽略分配和回收空間的時間,構造與析構的時間復雜度均為O(1)
6.動態空間管理
裝填因子:向量實際規模與其內部數組容量的比值(size/capacity),它是衡量空間利用率的指標
靜態空間管理:比如數組,數組在其生命周期內大小固定,一方面,數組容量過小,很容易出現溢出,而另一方面,數組容量過大,導致空間利用率低
動態空間管理的目的就是,使得空間既能足夠大以致不會溢出,也不會太大使得空間浪費,歸納起來就是,使得裝填因子始終在(0,1)這個區間上
向量擴容:
1 template <typename T> void Vector<T>::expand() 2 { 3 //尚未滿,不必擴容 4 if(_size<_capacity) 5 return; 6 //不得低於最小容量 7 if(_capacity<DEFAULT_CAPACITY) 8 _capacity=DEFAULT_CAPACITY; 9 T* oldElem=_elem; 10 //容量加倍 11 _elem=new T[_capacity<<1]; 12 //復制原向量內容 13 for(int i=0;i<_size;i++) 14 _elem[i]=oldElem[i]; 15 //釋放原空間 16 delete [] oldElem; 17 }
這裏選擇容量加倍是很有講究的,在這之前需要先了解分攤分析。
分攤分析:
對於一個操作的序列來講,平攤分析得出的是在特定問題中這個序列下每個操作的平攤開銷。一個操作序列中可能存在一、兩個開銷比較大的操作,在一般地分析下,如果割裂了各個操作的相關性或忽視問題的具體條件,那麽操作序列的開銷分析結果就可能會不夠緊確,導致對於操作序列的性能做出不準確的判斷。用平攤分析就可以得出更好的、更有實踐指導意義的結果。因為這個操作序列中各個操作可能會是相互制約的,所以開銷很大的那一兩個操作,在操作序列總開銷中的貢獻也會被削弱和限制。所以最終會發現,對於序列來講,每個操作平攤的開銷是比較小的。
換句話說,對於一個操作序列來講,平攤分析得出的是這個序列下每個操作的平攤開銷。
每次擴容,系統都要將原向量中的元素進行復制,這需要花費額外的時間,那麽,擴容一倍的策略時間復雜度即為O(2n)=O(n),這種策略的效率貌似很低,但O(n)僅僅是相對單次擴容而言。可以知道,經過一次擴容後,至少要再經過n次操作,才需要再一次擴容,而隨著向量規模逐漸增大,n也越來越大,那麽需要進行擴容操作的概率也就越來越小,平均而言,加倍擴容的成本並不很高。
縮容:一般情況下,下溢並不常見,需要用到縮容的場合很少,但不排除某些場合對空間利用率要求較高
1 template <typename T> void Vector<T>::shrink() 2 { 3 if(_capacity<DEFAULT_CAPACITY<<1) 4 return; 5 if(_size<<2>_capacity) //以25%為界 6 return; 7 T* oldElem=_elem; 8 //容量減半 9 _elem=new T[_capacity>>=1]; 10 //復制原向量內容 11 for(int i=0;i<_size;i++) 12 _elem[i]=oldElem[i]; 13 //釋放原空間 14 delete [] oldElem; 15 }
這裏選取25%為界,一旦空間利用率降至25%以下,將執行縮容操作。
實際應用中,為避免頻繁縮容,可使用更低閥值,取0時即為禁止縮容。
7.引用元素
向量ADT為我們提供了get操作,實際應用中,可以像數組一樣用下標運算符獲取元素。
但向量中的元素並不一定是基本類型,這時需要對下標運算符“[]”進行重載。
1 template <typename T> T& Vector<T>::operaator[](Rank r) const 2 {return _elem[r];}
8.向量置亂算法
1 template <typename T> void permute(Vector<T>& V) 2 { 3 for(int i=V.size();i>0;i--) 4 swap(V[i-1],V[rand()%i]); 5 }
應用於軟件測試,仿真模擬等方面,保證測試的覆蓋面和仿真的真實性
封裝置亂算法
1 template <typename T> void Vector<T>::unsort(Rank lo, Rank hi) 2 { 3 T* V = _elem + lo; 4 for (int i = V.size(); i > 0; i--) 5 swap(V[i - 1], V[rand() % i]); 6 }
這樣封裝以後,就可以對外提供一個置亂接口,可置亂任意區間[lo,hi]之間的元素
9.判等器和比較器
系統提供的比較符號包括“==”,“<=”,">="都是僅適用於數值類型的數據的,而向量的元素類型並不局限於基本類型,所以,要實現判等器和比較器,核心在於實現它們的通用性。
通常采用對比較操作進行封裝形成比較器或在定義相應的數據類型時重載“==”,“>=”等運算符
10.查找
無序向量
有序向量
11.插入
12.刪除
13.去重
無序向量
有序向量
14.遍歷
15.排序
數據結構與算法分析 - 3 - 向量