C++ STL容器總結

tcfellow發表於2018-01-11

Hints:在本文中,重點總結的是各個容器中增刪查的演算法時間複雜度。

在c++中,容器指的是能夠容納各種資料型別的通用資料資料結構,是類别範本。 比如set類别範本:

template <class _Key, class _Compare = less<_Key>,
          class _Allocator = allocator<_Key> >
class _LIBCPP_TYPE_VIS_ONLY set
複製程式碼

C++的容器主要分為三種:

  • 順序容器
  • 關聯容器
  • 容器介面卡

而訪問容器元素我們需要迭代器(引入容器標頭檔案就可以了,不需要單獨因為標頭檔案),迭代器有以下幾個屬性:

  • 用於指向順序容器和關聯容器的元素
  • 用法和指標類似
  • 有const(容器類名::iterator 變數名)和非const兩種(容器類名::const_iterator 變數名)
  • 通過迭代器可以讀取它指向的元素
  • 非const迭代器和可以修改其指向的元素

順序容器

何為順序容器,也就是說元素的位置是順序插入的,插入位置與元素的值無關,核心是容器沒有排序。 順序容器都具有以下10個成員函式:

#返回迭代器
begin
end
rbegin  //返回指向最後一個元素的迭代器
rend
#返回元素引用
front
back
#其他
erase(刪除區間時返回被刪除元素後面的迭代器;也可刪除一個或幾個元素)
clear
push_back
pop_back
複製程式碼

順序容器有以下三種:

1.vector動態陣列 標頭檔案<vetor>

元素在記憶體中是連續存放的。

隨機存取時間:常數時間(因為可以通過下標直接訪問到地址)。

在尾部增刪元素通常是常數時間(正常是常數時間,如果超出了預設分配的元素個數,會重新分配儲存空間,此時會消耗更多時間)。

在中間或者頭部增刪元素:o(n)(會移動其他元素的位置)。

迭代器型別:隨機訪問(支援下標訪問、隨機移動,例:a[i])。

查詢時間:o(n)(因為沒有排序,只能現行查詢,效率較低)。

可見vector在中間或者頭部增刪元素效能較低。

優點:記憶體和C完全相容、高效隨機訪問、節省空間

缺點:內部插入刪除元素代價巨大、動態大小查過自身容量需要申請大量記憶體做大量拷貝

建構函式:

 vector();
 vector(int n);
 vector(int n, const T &a); //把n個元素初始化為a
 vector(iterator first,iterator last); //初始化為其他容器上區間[first,last)一致的內容
複製程式碼

常用成員函式

void pop_back();
void push_back(const T &val);
int size();
T & font();
T & back();
複製程式碼

deque[讀:dek,之前經常讀dkju:]雙向佇列 標頭檔案<deque>

元素在記憶體中連續存放(連續記憶體的容器有個明顯的缺點,就是有新元素插入或老元素刪除的時候,為了給新元素騰出位置或者填充老元素的空缺,同一塊記憶體中的其他資料需要進行整體的移位,這種移位的拷貝代價有時是非常巨大的。deque實際上是分配到不同記憶體塊,通過連結串列把記憶體塊連在一起,再進行連續存放,是list與vector的折中)。

隨機存取時間:常數時間(僅次於vector,因為有可能存在尾部的記憶體位置在頭部之前的場景)。

兩端增刪元素通常是常數時間。(deque不像vector沒有容量,不需要重新分配記憶體空間。這是因為deque由動態分配的連續空間,即緩衝區,組合而成,隨時可以增加一段新的空間連結起來。它沒有必要像vector那樣“因舊空間不足而重新分配2倍的空間,然後複製元素,再釋放舊空間”。當重新分配緩衝區時,耗時增加)。

在中間插入:時間複雜度較高。

迭代器型別:隨機訪問(效率低於vector)。

查詢時間:o(n)(原因同上)。

**優點:高效隨機訪問、內部插入刪除元素效率方便、兩端push、pop效率很高 **

**缺點:記憶體佔用比較高 **

list雙向連結串列 標頭檔案<list>

元素在記憶體中不連續分配(因為指標可以獲取前後元素的地址),所以不支援隨機存取

在任何位置增刪元素時間:常數時間。

查詢時間:o(n)(原因同上)。

迭代器型別:雙向(不支援下標訪問,不支援迭代器的比較運算子,和+-運算子)

優點:任意位置插入刪除元素常量時間複雜度、兩個容器融合是常量時間複雜度

缺點:不支援隨機訪問、比vector佔用更多的儲存空間

常用成員函式:(注意這些在順序容器中都是list獨有的)

push_front
pop_front
sort   //不支援STLsort演算法
remove
unique
merge
reverse
splice
複製程式碼

特別說明,list的sort函式有無參和compare兩個版本

list<T> classname
classname.sort(compare);  //compare自定義
classname.sort();
複製程式碼

關聯容器(迭代器型別:雙向)

關聯的意思就是元素是排序的。

插入與檢索元素時間: o(log(N))(因為通過紅黑二叉樹實現,時間與平衡二叉樹一致)。

需要注意的是,STL中有些演算法比如sort,binary_search需要通過隨機訪問迭代器訪問容器中的元素,因此list以及關聯容器就不能支援該演算法!

主要包括以下4種:

  • set
  • multiset
  • map
  • multimap

除了個容器都有的成員函式外,它們都有以下成員函式

find
lower_bound
upper_bound
equal_range
count
insert
複製程式碼

map與set的不同點在於,map中存放的元素有且僅有兩個成員變數,first(類似於key),second(類似於value),map可以根據first對元素排序和檢索。

set與multiset的不同在於,後者允許存在相同值的元素。 map與multimap的不同在於,後者允許存在相同的first值的元素。

容器介面卡(不支援迭代器)

這個概念聽起來有點抽象,其實就是STL幫我們抽象了實際工作中所需要的資料操作方式。舉個例子,交流電可以有任意的傳輸電壓,但是實際生活中我們只需要220v就夠了,變壓器幫我們實現了這個基本的功能。同理,容器的操作方式多種多樣,但是其實我們只需要他們按照實際的一些規範來操作即可。

他們都有以下3個成員函式:

push
top
pop
複製程式碼

STL中的排序,查詢,變序演算法都不適合容器介面卡(因為容器介面卡的元素位置不可隨意改變,並且訪問有一定的訪問規則,不支援隨機訪問)。

stack 棧 標頭檔案<stack>

是一個有限序列,並滿足序列中被刪除、檢索和修改的項只能是最近插入序列的項(棧頂項)。也即,先進先出LIFO

可以用vector,deque,list(效能最差)實現,編譯器預設是用deque實現的。

比如,在程式除錯中,錯誤堆疊就是一種應用。

queue 佇列 標頭檔案 <queue>

插入只可以在尾部進行,刪除、檢索和修改只允許從頭部進行。先進先出FIFO

queue由於pop,push發生在對頭,所以不能用vector實現,可以用list,deque實現,編譯器預設用deque實現。

比如,應用於需要保證訊息順序性的場景,如訊息佇列。

priority_queue 優先順序佇列 標頭檔案<queue>

最高優先順序元素總是第一個出列。

它需採用堆排序來保證元素總是在最前面,因此要求隨機訪問,所以不能使用list實現,可以用vector和deque實現,編譯器預設情況下用vector實現。

比如,作業系統的執行緒的排程演算法,有的是按照優先順序來排程的。

總結

  • 如果需要隨機訪問,用vector

  • 如果儲存元素的數目已知,用vector

  • 需要任意位置隨機插入刪除,用list

  • 經常在容器的首部尾部插入刪除元素,用deque

  • 元素結構複雜用list,也可以用vector儲存指標(需要額外的精力去維護記憶體),看需求

  • 如果操作是基於鍵值,用set/map

  • 如果需要經常的搜尋,用map/set

參考:

C++ STL容器時間複雜度下的最佳選擇

深入理解deque容器

優先佇列及最小堆最大堆