STL 即標準模板庫(Standard Template Library),是 C++ 標準庫的一部分,裏面包含了一些模板化的通用的數據結構和算法。STL 基於模版的實現,所以可以支持自定義的數據結構。html
STL 中一共有 6 大組件:node
參考資料:c++
仿函數 (Functor) 的本質就是在結構體中重載 ()
運算符。算法
例如:設計模式
struct Print { void operator()(const char *s) const { cout << s << endl; } }; int main() { Print p; p("hello"); }
這一律念將會在 priority_queue
中使用(在智能指針的 unique_ptr
自定義 deleter 也會用到)。數組
容器 (Container) 在 STL 中又分爲序列式容器 (Sequence Containers) ,關聯式容器 (Associative Containers) 和無序容器 (Unorderde Containers) .網絡
常見的序列式容器包括有:vector, string, array, deque, list, forward_list
.數據結構
vector/stringless
底層實現:vector
是內存連續、自動擴容的數組,實質仍是定長數組。數據結構和算法
特色:
[]
運算符size == capacity
時,那麼擴容爲當前容量的 2 倍,並拷貝原來的數據==, !=, <, <=, >, >=
比較運算
<=>
運算符 (aka, three-way comparsion )。PS:string
的底層實現與 vector
是相似的,一樣是內存連續、自動擴容的數組(但擴容策略不一樣)。
array (C++11)
底層實現:array
是內存連續的 、 固定長度的數組,其本質是對原生數組的直接封裝。
特色(主要是與 vector
比較):
[]
隨機訪問pop_front/back, erase, insert
這些操做。vector
的初始化方式爲函數參數(如 vector<int> v(10, -1)
,長度可動態肯定),但 array
的長度須要在編譯期肯定,如 array<int, 10> a = {1, 2, 3}
.須要注意的是,array
的 swap
方法複雜度是 \(\Theta(n)\) ,而其餘 STL 容器的 swap
是 \(O(1)\),由於只須要交換一下指針。
deque
又稱爲「雙端隊列」。
底層實現:多個不連續的緩衝區,而緩衝區中的內存是連續的。而每一個緩衝區還會記錄首指針和尾指針,用來標記有效數據的區間。當一個緩衝區填滿以後便會在以前或者以後分配新的緩衝區來存儲更多的數據。
特色:
[]
隨機訪問list
底層實現:雙向鏈表。
特色:
[]
隨機訪問forwar_list (C++11)
底層實現:單向鏈表。
特色:
list
減小了空間開銷[]
隨機訪問rbegin(), rend()
關聯式容器包括:set/multiset
,map/multimap
。multi
表示鍵值可重複插入容器。
底層實現:紅黑樹。
特色:
自定義比較方式:
<
int
等內置類型,經過仿函數struct cmp { bool operator()(int a, int b) { return a > b; } }; set<int, cmp> s;
無序容器 (Unorderde Containers) 包括:unordered_set/unordered_multiset
,unordered_map/unordered_multimap
.
底層實現:哈希表。在標準庫實現裏,每一個元素的散列值是將值對一個質數取模獲得的,
特色:
在實際應用場景下,假設咱們已知鍵值的具體分佈狀況,爲了不大量的哈希衝突,咱們能夠自定義哈希函數(仍是經過仿函數的形式)。
struct my_hash { size_t operator()(int x) const { return x; } }; unordered_map<int, int, my_hash> my_map; unordered_map<pair<int, int>, int, my_hash> my_pair_map;
四種操做的平均時間複雜度比較:
Containers | 底層結構 | 增 | 刪 | 改 | 查 |
---|---|---|---|---|---|
vector/deque |
vector: 動態連續內存 deque: 連續內存+鏈表 |
\(O(n)\) | \(O(n)\) | \(O(1)\) | \(O(n)\) |
list |
雙向鏈表 | \(O(1)\) | \(O(1)\) | \(O(1)\) | \(O(n)\) |
forward_list |
單向鏈表 | \(O(1)\) | \(O(n)\) | \(O(1)\) | \(O(n)\) |
array |
連續內存 | 不支持 | 不支持 | \(O(1)\) | \(O(n)\) |
set/map/multiset/multimap |
紅黑樹 | \(O(\log{n})\) | \(O(\log{n})\) | \(O(\log{n})\) | \(O(\log{n})\) |
unordered_{set,multiset} unordered_{map,multimap} |
哈希表 | \(O(1)\) | \(O(1)\) | \(O(1)\) | \(O(1)\) |
容器適配器 (Container Adapter) 其實並非容器(我的理解是對容器的一種封裝),它們不具備容器的某些特色(如:有迭代器、有 clear()
函數……)。
常見的適配器:stack
,queue
,priority_queue
。
對於適配器而言,能夠指定某一容器做爲其底層的數據結構。
stack
deque
top, pop, push, size, empty
操做的時間複雜度均爲 \(O(1)\) 。指定容器做爲底層數據結構:
stack<TypeName, Container> s; // 使用 Container 做爲底層容器
queue
deque
front, push, pop, size, empty
操做的時間複雜度均爲 \(O(1)\) 。指定容器:
queue<int, vector<int>> q;
priority_queue
又稱爲 「優先隊列」 。
vector
top, empty, size
push, pop
模版參數解析:
priority_queue<T, Container = vector<T>, Compare = less<T>> q; // 經過 Container 指定底層容器,默認爲 vector // 經過 Compare 自定義比較函數,默認爲 less,元素優先級大的在堆頂,即大頂堆 priority_queue<int, vector<int>, greater<int>> q; // 傳入 greater<int> 那麼將構造一個小頂堆 // 相似的,還有 greater_equal, less_equal
迭代器 (Iterator) 實際上也是 GOF 中的一種設計模式:提供一種方法順序訪問一個聚合對象中各個元素,而又不需暴露該對象的內部表示。
迭代器的分類以下圖所示。
STL 中各容器/適配器對應使用的迭代器以下表所示。
Container | Iterator |
---|---|
array | 隨機訪問迭代器 |
vector | 隨機訪問迭代器 |
deque | 隨機訪問迭代器 |
list | 雙向迭代器 |
set / multiset | 雙向迭代器 |
map / multimap | 雙向迭代器 |
forward_list | 前向迭代器 |
unordered_{set, multiset} | 前向迭代器 |
unordered_{map, multimap} | 前向迭代器 |
stack | 不支持迭代器 |
queue | 不支持迭代器 |
priority_queue | 不支持迭代器 |
迭代器失效是由於向容器插入或者刪除元素致使容器的空間變化或者說是次序發生了變化,使得原迭代器變得不可用。所以在對 STL 迭代器進行增刪操做時,要格外注意迭代器是否失效。
網絡上搜索「迭代器失效」,會發現不少這樣的例子,在一個 vector
中去除全部的 2 和 3,故意用一下迭代器掃描(你們都知道能夠用下標):
int main() { vector<int> v = {2, 3, 4, 6, 7, 8, 9, 3, 2, 2, 2, 2, 3, 3, 3, 4, 5, 6}; for (auto i = v.begin(); i != v.end(); i++) { if (*i==2 || *i==3) v.erase(i), i--; // correct code should be // if (*i==2 || *i==3) i=v.erase(i), i--; } for (auto i = v.begin(); i != v.end(); i++) cout << *i << ' '; }
我好久以前用 Dev C++ (應該是內置了很古老的 MinGW)寫代碼的時候,印象中也遇到過這種狀況,v.erase(i), i--
這樣的操做是有問題的。 erase(i)
會使得 i
及其後面的迭代器失效,從而發生段錯誤。
但如今 MacOS (clang++ 12), Ubuntu16 (g++ 5.4), Windows (mingw 9.2) 上測試,這段代碼都沒有問題,而且能輸出正確結果。編譯選項爲:
g++ test.cpp -std=c++11 -O0
實際上也不難理解,由於是連續內存,i
指向的內存位置,在 erase
以後被其餘數據覆蓋了(這裏的行爲就跟咱們使用普通數組同樣),但該位置仍然在 vector
的有效範圍以內。在上述代碼中,當 i = v.begin()
時,執行 erase
後,對 i
進行自減操做,這已是一種未定義行爲。
我猜應該是 C++11 後(或者是後來的編譯器更新),對迭代器失效的這個問題進行了優化。
雖然可以正常運行,但我認爲最好仍是嚴謹一些,更嚴格地遵循迭代器的使用規則:if (*i==2 || *i==3) i=v.erase(i), i--;
.
如下爲各種容器可能會發生迭代器失效的狀況:
vector, deque
)
insert(i)
和 erase(i)
會發生數據挪動,使得 i
後的迭代器失效,建議使用 i = erase(i)
獲取下一個有效迭代器。vector
自動擴容時,可能會申請一塊新的內存並拷貝原數據(也有多是在當前內存的基礎上,再擴充一段連續內存),所以全部的迭代器都將失效。list, forward_list
):insert(i)
和 erase(i)
操做不影響其餘位置的迭代器,erase(i)
使得迭代器 i
失效,指向數據無效,i = erase(i)
可得到下一個有效迭代器,或者使用 erase(i++)
也可(在進入 erase
操做前已完成自增)。set/map
):與鏈表型相同。unodered_{set_map}
):與鏈表型相同。